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.
Install clj-kondo and create the lint cache:
mkdir -p .clj-kondo
clj-kondo --lint "$(lein classpath)" --dependencies --parallel --copy-configs
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 package.json script for that:
npm run 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
lein jar
If the library needs to be referenced locally:
lein install
To deploy the library to Clojars:
lein deploy clojars
These actions are unified in Leiningen release process:
lein release :patch
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.2.2"]
In deps.edn:
org.theparanoidtimes/cljs-map-pins {:mvn/version "0.2.2"}
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.2.2"]
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))
See the API documentation here.
For any suggestions, bug reports, ideas and patches please send a plain-text e-mail
to ~tpt/tpt-dev@lists.sr.ht.
Copyright (C) 2022-2026. 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/.
Can you improve this documentation?Edit on sourcehut
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |