~tpt/cljs-map-pins

cljs-map-pins is a ClojureScript library for drawing a set of pins from an EDN file on a world map
6 hours ago
6 hours ago

#Table of Contents

  1. cljs-map-pins
    1. Development
      1. Testing
    2. Deployment
    3. Usage
    4. License

#cljs-map-pins

img img

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.

#Development

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.

#Testing

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.

#Deployment

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

#Usage

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.

#License

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/.