cljs-map-pins
is a ClojureScript library for drawing a set of pins
from an EDN file on a world map. It relies on OpenLayers (OL) for map
drawing and it abstracts the setup and drawing code allowing
the user to specify only the pin coordinates and the way they
should be displayed and how will they behave.
As it is an abstraction over the OpenLayers library all options for
drawing a map are available in cljs-map-pins
, they are simply passed
to OL.
A custom type keyword can be assigned to each pin to ensure a special
on-click behavior on the map. cljs-map-pins
accepts a map of types to
handler implementations that will be invoked on the on-click event.
Each pin can support its own style - they way it is displayed on the map,
and it can be an image resource or any style supported by OL.
cljs-map-pins
is a Leiningen project and uses shadow-cljs framework.
Previously, the target framework was Figwheel that used OL as a cljsjs/packages dependency. Since that project is now deprecated it was only logical to migrate to shadow-cljs (as of version 0.2.0). Migration was a large undertaking and the shadow-cljs User's guide was an essential reference.
shadow-cljs relies on NPM to install JavaScript dependencies,
so NodeJS must be installed. Yarn can be installed, but it is
not necessary for the development. Shortcut scripts are defined
in package.json
file that can be invoked with Yarn instead of
invoking multiple other commands.
With NPM install shadow-cljs Node module as a development dependency:
npm install --save-dev shadow-cljs
To avoid always referencing the project local binary, install the shadows-cljs module as a global package too:
npm install -g shadow-cljs
This way shadow-cljs
executable will be available everywhere.
To install all NPM dependencies:
npm install
To start an interactive development environment run:
shadow-cljs watch app
# or with Yarn
yarn watch
This will create a running server process that will auto compile
all changes. Development page will be served on http://localhost:8700
.
shadow-cljs server page will be on http://localhost:9630
.
The page is empty, but its entry point is the cljs-map-pins.user
namespace
that is watched by the build and contains useful functions for development.
Open the cljs-map-pins.user
namespace file in the editor and
create a map for development and exploratory testing:
;; this will draw a map on the development page
;; and bind it to m (type ol.Map)
(def m (draw!))
;; to erase the map
(erase! m)
Any changes made to this file (or to the core
) file will be automatically
reflected on the page.
cljs-map-pins
tests are run in a browser environment, and shadow-cljs
uses Karma test runner for this.
To install Karma and the required runners as development dependencies:
npm install --save-dev karma karma-chrome-launcher karma-cljs-test
Same as for shadow-cljs, to avoid referencing the project local binary install Karma module globally.
npm install -g karma-cli
karma
executable will now be available everywhere.
Target folders must be cleaned before each test and there is a Leiningen alias for that:
lein full-clean
# or with Yarn
yarn clean
To start an interactive test session in the Chrome browser window
first compile the test
build with shadows-cljs. When done Karma
test run will reference the compiled files and open the browser window.
shadow-cljs compile test
karma start karma-test.conf.js
# or with Yarn
yarn test
This will start the Karma test server on http://localhost:9876
in Chrome.
The map for testing with a couple of features and a simple behavior is
on http://localhost:9876/debug.html
page. The server will stay alive
and even reopen Chrome if accidentally closed for as long as needed.
The previous build is more for exploratory testing. There is a different test configuration for fast testing in the headless Chrome environment:
shadow-cljs compile test
karma start karma-headless.conf.js --single-run
# or with Yarn
yarn headless
This will compile and execute all tests in a Chrome headless environment.
The library is packaged as a jar without any resources.
lein clean # or lein full-clean
lein jar
If the library needs to be referenced locally:
lein install
To deploy the library to Clojars:
lein deploy clojars
cljs-map-pins
exposes a single function for drawing: draw-pins!
.
It accepts four arguments: a goog.dom
element where the map will
be drawn, OL map of options, a map (EDN) of pins that will be drawn
called features and a mapping of pin types to custom handler functions.
OpenLayers options are passed to the OL instance (ol.View
object) unchanged.
The full list and description is here. The options map is validated by cljs-map-pins
before passing it to the OL instance. The options map can contain the :controls
key
that accepts a vector of ol.control.Control instances. These are passed to the map
instance in the same way as in native OpenLayers. The default behavior is an empty
controls vector, but cljs-map-pins
always adds an ol.control.Attribution
instance.
To suppress this add the :withoutAttribution
key to the options map with the true
value. It is recommended to always add OpenStreetMap attribution so ensure that
if :withoutAttribution
is true
, a custom instance of ol.control.Attribution
is
added to the control vector.
The features are displayed on the map by displaying the ol.style.Style instance
from the :style
key on the provided :coordinates
vector on the map. The coordinates
format must be usable by the projection defined in the options map. If a feature
is to be displayed using an image (a pin image for example) :img
keyword can be
used instead of :style
. The path to the given image should be associated to
the :img
keyword and cljs-map-pins
will create an ol.style.Style
instance automatically.
The content of the feature is stored in :name
, :title
and :description
keywords. What
and how the content is shown is up to the feature handler implementation.
Each feature type should have a FeatureHandler
instance associated to it. These
handlers are called on the 'on-click' event and the whole feature map is passed
as an argument to the handle
function.
The library is released to Clojars so it can be included as a dependency.
In project.clj
:
[org.theparanoidtimes/cljs-map-pins "0.1.0"]
In deps.edn
:
org.theparanoidtimes/cljs-map-pins {:mvn/version "0.1.0"}
If your application uses the shadow-cljs framework, add you cljs-map-pins
dependency
to the shadow-cljs.edn
file:
[org.theparanoidtimes/cljs-map-pins "0.1.0"]
The following usage example shows how to draw a simple map with two features.
Since one of the features uses a specific OL style implementation, OL JavaScript
files must be required, and for that to work the project must use shadow-cljs.
If a project doesn't need to touch OL, maybe it can get away with using cljs-map-pins
as a normal dependency.
(require '[goog.dom :as gdom])
(require '[cljs-map-pins.core :as mp])
(require '["ol/control" :as olc :refer (Zoom)])
(require '["ol/style" :as ols :refer (Style RegularShape Fill Stroke)])
;; reference the DOM element where the map will be drawn
(def map-element (gdom/getElement "map"))
;; OpenLayers map options
(def ol-options
{:center [20.459526 44.815500]
:extent [18.5 40.4 22.7 47.5]
:zoom 10
:minZoom 9
:maxZoom 17
:projection "EPSG:4326"
:controls [(olc/Zoom.)]})
;; a vector of features that will be drawn on the OL map
(def features
[{:name "Feature 1"
:type :normal
:title "Map pin"
:description "This is the first feature on the map."
:url "https://example.com"
:coordinates [20.459526 44.815500]
:img "img/pin.png"}
{:name "Feature 2"
:type :special
:title "Map triangle shape"
:description "This is the second feature on the map."
:coordinates [20.450938469037453 44.8224132937195]
:style (ols/Style. #js {:image (ols/RegularShape. #js {:fill (ols/Fill. #js {:color "red"})
:stroke (ols/Stroke. #js {:color "black"
:width 2})
:points 3
:radius 10
:rotation (/ (.-PI js/Math) 4)
:angle 0})})}])
;; the default on-click handler which is an instance of FeatureHandler
(def default-type-handler
(reify mp/FeatureHandler
(handle [this feature]
(do-something-in-dom! feature))))
;; the on-click handler for features of type :special
(def special-type-handler
(reify mp/FeatureHandler
(handle [this feature]
(do-something-special-in-dom! feature))))
;; the no-op function that will be called on on-click event without a feature
(defn noop-handler
[]
(reset-dom-state!))
;; The mapping of handlers to feature types
;; each feature type should be mapped to a handler instance,
;; and if not, the default handler will be used.
;; The reserved keywords are :default which maps to the
;; default handler instance, and :no-feature wich maps
;; to the function that will be called when an 'on-click' events
;; happens on a map part without a feature.
(def handlers
{:normal default-type-handler
:special special-type-handler
:default default-type-handler
:no-feature noop-handler})
;; function for drawing the map which returns an instance of ol.Map JS object
(def ol-map (mp/draw-pins! map-element ol-options features handlers))
Full library documentation is in doc/api
.
Copyright (C) 2022-2023. Dejan Josifović The Paranoid Times
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.