#+TITLE: Embedding maps in your apps #+DESCRIPTION: Example's of embeding maps into your frontend application. #+FILETAGS: clojurescript:frontend:interop * Introduction Below you will find some simple examples of using map api's inside clojure, you can use the inline eval for some of the example where sci supports a specific library. Install npm dependencies ** Setup for downloaded version Install the npm dependencies for react then launch shadow via the terminal or jack in via your IDE. #+BEGIN_SRC html :results silent :exports none :tangle readme.org Install dependencies with. npm install Run the project with the below command. npx shadow-cljs watch app Alternatively jack into the project from your ide. #+END_SRC * Google Maps Below is an example of using google maps, it pulls in the library directly you could alternatively use an npm dependency. You will need to provide your own api key, the examples use no api key and render a warning so paste in your google api key to make this work. #+BEGIN_SRC html :results silent :exports none :tangle resources/public/index.html Clojure demos
App loading here
#+END_SRC #+BEGIN_SRC edn :results silent :exports none :tangle deps.edn {:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.10.0"} org.clojure/clojurescript {:mvn/version "1.11.60"} funcool/promesa {:mvn/version "11.0.674"} reagent/reagent {:mvn/version "1.2.0"} thheller/shadow-cljs {:mvn/version "2.24.0"}}} #+END_SRC #+BEGIN_SRC edn :results silent :exports none :tangle shadow-cljs.edn {:deps {:aliases [:dev]} :dev-http {8080 ["resources/public/" "classpath:public"]} :source ["src" "../../components"] :builds {:app {:output-dir "resources/public/cljs-out/" :asset-path "/cljs-out" :target :browser :compiler-options {:infer-externs :auto :externs ["datascript/externs.js"] :output-feature-set :es6} :modules {:main_bundle {:init-fn clojure-demo.core/startup!}} :devtools {:after-load app.main/reload!}}}} #+END_SRC #+BEGIN_SRC json :results silent :exports none :tangle package.json { "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "shadow-cljs": "^2.23.3", "webpack": "^5.74.0", "webpack-cli": "^4.10.0" } } #+END_SRC #+BEGIN_SRC json :exports none :results output :tangle src/clojure_demo/core.cljs (ns clojure-demo.core (:require ["react-dom/client" :refer [createRoot]] [cljs.core.async :as async] [cljs.core.async.interop :as async-in] [promesa.core :as promesa] [shadow.cljs.modern :refer [js-await] :as shadow] [reagent.core :as reagent])) #+END_SRC ** Load in the google maps script by adding it to the dom Code pulled from google #+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs (defn load-google-maps-script [api-key] (let [script (.createElement js/document "script")] ;; copied from googles recommended way of loading google maps (set! (.-innerHTML script) (str "(g=>{var h,a,k,p=\"The Google Maps JavaScript API\",c=\"google\",l=\"importLibrary\",q=\"__ib__\",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement(\"script\"));e.set(\"libraries\",[...r]+\"\");for(k in g)e.set(k.replace(/[A-Z]/g,t=>\"_\"+t[0].toLowerCase()),g[k]);e.set(\"callback\",c+\".maps.\"+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+\" could not load.\"));a.nonce=m.querySelector(\"script[nonce]\")?.nonce||\"\";m.head.append(a)}));d[l]?console.warn(p+\" only loads once. Ignoring:\",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({ key: \"" api-key "\", v: \"weekly\"});")) (.appendChild (.-head js/document) script))) #+END_SRC ** Create a map using google and core async In this example we render google maps using core async, async go blocks drop type hints so we need to pull out any hinting to functions, this is a shame as this version reduces the amount of nesting. #+BEGIN_SRC clojurescript :results none :tangle src/clojure_demo/core.cljs (defn google-map-core-async [] (let [map-element (reagent/atom nil) ;; pulled out of the go block to satisfy the infer warnings, the go block removes hints get-map-obj (fn [obj] (.-Map ^js/google.maps.Map obj)) get-marker-obj (fn [obj] (.-Marker ^js/google.maps.Map obj))] (load-google-maps-script "") ;; go blocks loose hints so may cause infer warnings (async/go (let [gMap (get-map-obj (async-in/js {:center {:lng 131.031 :lat -25.344} :zoom 4}))] (gMarker. (clj->js {:map map :title "test marker 1" :position {:lng 131.031 :lat -24.344}}) "marker 1") (gMarker. (clj->js {:map map :title "test marker 2" :position {:lng 131.031 :lat -25.344}}) "marker 2"))) (fn [] [:div#google-map-async.m-auto {:style {:width "400px" :height "400px"} :ref #(reset! map-element %)} "core async map here"]))) #+END_SRC ** Create a map using google and shadow js-await Shadow CLJS has its one js-await macro we can use in the following fashion, here we just use it to wait for the maps library to have loaded the maps and marker libraries before executing the code. #+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs (defn google-map-js-await [] (let [map-element (reagent/atom nil)] (load-google-maps-script "") (shadow/js-await [js-map (js/google.maps.importLibrary "maps")] (shadow/js-await [js-marker (js/google.maps.importLibrary "marker")] (when @map-element (let [gMap (.-Map ^js/google.maps.Map js-map) gMarker (.-Marker ^js/google.maps.Marker js-marker) map (gMap. @map-element (clj->js {:center {:lng 131.031 :lat -25.344} :zoom 4}))] (gMarker. (clj->js {:map map :title "test marker 1" :position {:lng 131.031 :lat -24.344}}) "marker 1") (gMarker. (clj->js {:map map :title "test marker 2" :position {:lng 131.031 :lat -25.344}}) "marker 2") nil)))) (fn [] [:div#google-map-js-await.m-auto {:style {:width "400px" :height "400px"} :ref #(reset! map-element %)} "shadow js-await map here"]))) [google-map-js-await] #+END_SRC ** Create a map using google and promesa An example using promesa to render a google map. #+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs (defn google-map-promesa [] (let [map-element (reagent/atom nil)] (load-google-maps-script "") (promesa/let [js-map (js/google.maps.importLibrary "maps") js-marker (js/google.maps.importLibrary "marker")] (let [gMap (.-Map ^js/google.maps.Map js-map) gMarker (.-Marker ^js/google.maps.Marker js-marker) map (gMap. @map-element (clj->js {:center {:lng 131.031 :lat -25.344} :zoom 4}))] (gMarker. (clj->js {:map map :title "test" :position {:lng 131.031 :lat -24.344}}) "marker 1") (gMarker. (clj->js {:map map :title "test" :position {:lng 131.031 :lat -25.344}}) "marker 1"))) (fn [] [:div#google-map-promesa.m-auto {:style {:width "400px" :height "400px"} :ref #(reset! map-element %)} "promesa map here"]))) [google-map-promesa] #+END_SRC #+BEGIN_SRC clojure :exports none :tangle src/clojure_demo/core.cljs (defn current-page [] [:div [google-map-core-async] [google-map-promesa] [google-map-js-await]]) (defn mount-root-page [] ;; this select the main node from the html file and injects your page content (.render (createRoot (.getElementById js/document "app")) (reagent/as-element [current-page]))) (def startup! (mount-root-page)) #+END_SRC * LibraMaps Create a dummy function to load the js dynamically, this could be imported in other ways like npm. #+BEGIN_SRC clojurescript :results value :tangle src/clojure_demo/core.cljs (defn load-libra-maps [] (let [script (.createElement js/document "script")] (.setAttribute script "src" "https://unpkg.com/maplibre-gl/dist/maplibre-gl.js") (.appendChild (.-head js/document) script))) (load-libra-maps) #+END_SRC We use =:ref= to grab a reference and call our function when it is created we can then get elements id and construct our map. To customize see https://maplibre.org/maplibre-gl-js/docs/ #+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs (defn libra-map-promesa [] (load-libra-maps) (let [create-map (fn map-element [element] (new js/maplibregl.Map (clj->js {:container (.-id element) #_"libra-map" :style "https://demotiles.maplibre.org/style.json" :zoom 1})))] (fn [] [:div#libra-map.m-auto {:style {:width "400px" :height "400px"} :ref create-map #_(reset! map-element %)} "libra map here"]))) [libra-map-promesa] #+END_SRC