Add a JMap NG App extension

JMap NG extensions are plug-in modules taht can exetend the functionalities present in JMap NG App. You can develop and add your own extensions.

For more information about JMap NG extensions, refer to this section.

The JMap NG App extension API documentation is available here.

A simple example

The example bellow shows a simple extension that creates a panel and displays some text on it.

Try it out in Codepen.io

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <meta charset="UTF-8">
    <style>
      html,
      body {
        padding: 0px;
        margin: 0px;
        height: 100%;
        width: 100%;
      }
    </style>
  </head>
  <body>
  	<script type="text/javascript">
      const extensionId = "my-extension"
      window.JMAP_OPTIONS = {
        restBaseUrl: "https://jmapdoc.jmaponline.net/services/rest/v2.0",
        anonymous: true,
        projectId: 1,
        map: {
          center: {
            x: -74.17355888315548,
            y: 45.504030591808856
          },
          zoom: 8.04872607654608
        },
        hideMainLayout: true,
        application: {
          panel: `${extensionId}-panel`,
          extensions: [{
            id: extensionId,
            panelIcon: "fa-unlock-alt", // this is a font-awesome icon, but you can pass an image url
            panelTitle: "My custom extension",
            panelTooltip: "My custom extension",
            onPanelCreation: containerId => {
              const container = document.getElementById(containerId)
              const span = document.createElement("span")
              span.id = "my-text"
              span.innerHTML = "This is a custom panel"
              container.append(span)
            },
            onPanelDestroy: (containerId) => {
              document.getElementById(containerId)
              const text = document.getElementById("my-text")
              if (text) {
                text.remove()
              }
            }
          }]
        }
      }
    </script>
    <script defer type="text/javascript" src="https://cdn.jsdelivr.net/npm/jmap-app-js@7_Kathmandu_HF3"></script>
  </body>
</html>

There is another way to load your extension, after the JMap NG App has been loaded.

You can register the previous extension like this:

JMap.Application.Extension.register({
  id: "my-extension",
  panelIcon: "fas fa-wrench",
  panelTitle: "My custom extension",
  panelTooltip: "My custom extension",
  onPanelCreation: containerId => {
    const container = document.getElementById(containerId)
    const span = document.createElement("span")
    span.id = "my-text"
    span.innerHTML = "This is a custom panel"
    container.append(span)
  },
  onPanelDestroy: (containerId) => {
    document.getElementById(containerId)
    const text = document.getElementById("my-text")
    if (text) {
      text.remove()
    }
  }
})

A more complete example

The example bellow shows you a more sophisticated extension named "Favourite locations".

It adds points on the map where you click, and displays a list of your "favourite" locations. You can also toggle the visibility of the point layer.

Try it out in Codepen.io

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <meta charset="UTF-8" />
    <style>
      html,
      body {
        padding: 0px;
        margin: 0px;
        height: 100%;
        width: 100%;
      }
    </style>
  </head>

  <body>
    <script type="text/javascript">
      function getFavouriteExtension(extensionId) {
        const LAYER_ID = "favourite-custom-layer"
        const CLICK_LISTENER_ID = "favourite-click-listener"
        const ACTION_ADD = "FAVOURITE_ADD_LOCATION"
        const ACTION_DEL = "FAVOURITE_DEL_LOCATION"
        const ACTION_SET_VISIBILITY = "FAVOURITE_SET_LAYER_VISIBILITY"
        let panelContainerId
        let visibilityContainerId
        let locationsContainerId
        let reduxUnsubscribe
        let map
        let previousLocationCount = 0
        let previousLayerVisibility = true
        // return the JMap application current redux state
        function getAppState() {
          return JMap.getDataStore().getState().external["jmap_app"]
        }
        // return the favourite extension current redux state
        function getFavouriteState() {
          return JMap.getDataStore().getState().external[extensionId]
        }
        // return the primary color in use by JMap Application
        function getPrimaryColor() {
          return getAppState().ui.theme.palette.text.primary
        }
        // return the secondary color in use by JMap Application
        function getSecondaryColor() {
          return getAppState().ui.theme.palette.text.secondary
        }
        /**
         * This is the favourite service.
         * It is where we change the redux store values,
         * then the reduxChangeHandler function will react to state changes.
         **/
        const favouriteSVC = {
          // returns all existing locations
          getLocations: () => getFavouriteState().locations.slice(),
          // return true if the layer is displayed on the map
          getLayerVisibility: () => getFavouriteState().isLayerVisible,
          // add a location in the redux store
          add: location =>
            JMap.getDataStore().dispatch({
              type: ACTION_ADD,
              location: location
            }),
          // remove a location in the redux store at the index
          del: index =>
            JMap.getDataStore().dispatch({
              type: ACTION_DEL,
              index: index
            }),
          // if false, hide the layer on the map, else show it
          setLayerVisibility: visibility =>
            JMap.getDataStore().dispatch({
              type: ACTION_SET_VISIBILITY,
              visibility: visibility
            })
        }
        /**
         * This is the redux reducer, a pure javascript function.
         * It reacts to the actions you dispatch, and changes the data in the store.
         **/
        function reduxReducer(currentFavouriteState, action) {
          if (!currentFavouriteState) {
            currentFavouriteState = {
              locations: [],
              isLayerVisible: previousLayerVisibility
            }
          }
          switch (action.type) {
            case ACTION_ADD: {
              const newFavouriteState = { ...currentFavouriteState, locations: currentFavouriteState.locations.slice() }
              newFavouriteState.locations.push(action.location)
              return newFavouriteState
            }
            case ACTION_DEL: {
              const newFavouriteState = { ...currentFavouriteState, locations: currentFavouriteState.locations.slice() }
              newFavouriteState.locations.splice(action.index, 1)
              return newFavouriteState
            }
            case ACTION_SET_VISIBILITY: {
              return { ...currentFavouriteState, isLayerVisible: action.visibility }
            }
          }
          return currentFavouriteState
        }
        // Create the dom elements to display the layer visibility checkbox
        function displayLayerVisibility() {
          if (!visibilityContainerId) {
            visibilityContainerId = `${panelContainerId}-visibility`
          }
          const panelContainer = document.getElementById(panelContainerId)
          panelContainer.style.color = getPrimaryColor()
          panelContainer.style.padding = "1rem"
          const container = document.createElement("div")
          container.id = visibilityContainerId
          const span = document.createElement("span")
          span.innerHTML = "Display locations on map"
          container.append(span)
          const input = document.createElement("input")
          input.type = "checkbox"
          input.style.cursor = "pointer"
          input.style.marginLeft = "1rem"
          input.checked = favouriteSVC.getLayerVisibility()
          input.onclick = () => favouriteSVC.setLayerVisibility(!favouriteSVC.getLayerVisibility())
          container.append(input)
          panelContainer.append(container)
        }
        // Create the dom elements to display the locations list
        function displayLocations() {
          if (!locationsContainerId) {
            locationsContainerId = `${panelContainerId}-locations`
          }
          const panelContainer = document.getElementById(panelContainerId)
          const container = document.createElement("div")
          container.id = locationsContainerId
          container.style.marginTop = "1rem"
          container.style.display = "flex"
          container.style.flexDirection = "column"
          const locations = favouriteSVC.getLocations()
          if (locations.length === 0) {
            const span = document.createElement("span")
            span.innerHTML = "Click on the map to add your favourite locations"
            span.style.color = getSecondaryColor()
            container.append(span)
          } else {
            for (var i = 0; i < locations.length; i++) {
              const location = locations[i]
              const index = i
              const locationContainer = document.createElement("div")
              locationContainer.style.marginTop = "1rem"
              locationContainer.style.display = "flex"
              locationContainer.style.alignItems = "center"
              locationContainer.style.justifyContent = "space-between"
              const locationSpan = document.createElement("span")
              locationSpan.innerHTML = `LNG = ${location.x.toFixed(5)} | LAT = ${location.y.toFixed(5)}`
              locationContainer.append(locationSpan)
              const button = document.createElement("button")
              button.innerHTML = "X"
              button.title = "Click to remove the location"
              button.style.cursor = "pointer"
              button.onclick = () => favouriteSVC.del(index)
              locationContainer.append(button)
              container.append(locationContainer)
            }
          }
          panelContainer.append(container)
        }
        // Remove in the dom the container, given the container id
        function removeContainer(containerId) {
          const container = document.getElementById(containerId)
          if (container) {
            container.remove()
          }
        }
        function refreshLocations() {
          removeContainer(locationsContainerId)
          displayLocations()
        }
        // Locations have changed in the store, we add or remove it in the mapbox source.
        function refreshLayer() {
          map.getSource(LAYER_ID).setData({
            type: "FeatureCollection",
            features: getFavouriteState().locations.map(l => ({
              type: "Feature",
              geometry: { type: "Point", coordinates: [l.x, l.y] }
            }))
          })
        }
        /**
         * The redux function that is called when the redux state changed.
         * If a favourite value has changed it refreshes the ui or the map.
         **/
        function reduxChangeHandler() {
          const state = getFavouriteState()
          if (previousLocationCount !== state.locations.length) {
            previousLocationCount = state.locations.length
            refreshLocations()
            refreshLayer()
          }
          if (previousLayerVisibility !== state.isLayerVisible) {
            previousLayerVisibility = state.isLayerVisible
            map.setLayoutProperty(LAYER_ID, "visibility", state.isLayerVisible ? "visible": "none")
          }
        }
        /**
         * The map interactor, more details on:
         * https://k2geospatial.github.io/jmap-app/latest/modules/jmap.map.interaction.html
         **/
        const mapInteractor = {
          init: mapboxMap => {
            map = mapboxMap
            map.addSource(LAYER_ID, {
              type: "geojson",
              data: { type: "FeatureCollection", features: [] }
            })
            map.addLayer({
              id: LAYER_ID,
              type: "circle",
              source: LAYER_ID,
              paint: {
                "circle-radius": 10,
                "circle-color": "#0066ff"
              }
            })
            JMap.Event.Map.on.click(CLICK_LISTENER_ID, params => {
              favouriteSVC.add(params.location)
            })
            JMap.Event.Map.deactivate(CLICK_LISTENER_ID)
          },
          activate: () => {
            JMap.Map.getMap().doubleClickZoom.disable()
            JMap.Event.Map.activate(CLICK_LISTENER_ID)
          },
          deactivate: () => {
            JMap.Map.getMap().doubleClickZoom.enable()
            JMap.Event.Map.deactivate(CLICK_LISTENER_ID)
          },
          terminate: () => {
            JMap.Event.Map.remove(CLICK_LISTENER_ID)
            map.removeLayer(LAYER_ID)
            map.removeSource(LAYER_ID)
          }
        }
        // Returns the extension object that will be registered by the JMap Application
        return {
          id: extensionId,
          panelIcon: "fa-thumbtack",
          panelTitle: "My favourite locations",
          panelTooltip: "My favourite locations",
          onPanelCreation: containerId => {
            if (!panelContainerId) {
              panelContainerId = containerId
            }
            displayLayerVisibility()
            displayLocations()
            reduxUnsubscribe = JMap.getDataStore().subscribe(reduxChangeHandler)
          },
          onPanelDestroy: () => {
            removeContainer(visibilityContainerId)
            removeContainer(locationsContainerId)
            reduxUnsubscribe()
          },
          interactor: mapInteractor,
          serviceToExpose: favouriteSVC,
          storeReducer: reduxReducer
        }
      }
      const extensionId = "favourite"
      window.JMAP_OPTIONS = {
        restBaseUrl: "https://jmapdoc.jmaponline.net/services/rest/v2.0",
        anonymous: true,
        projectId: 1,
        map: {
          center: {
            x: -73.92251114687645,
            y: 45.52559621002098
          },
          zoom: 9.07038517536697
        },
        hideMainLayout: true,
        application: {
          panel: `${extensionId}-panel`, // will display the favourite panel when application starts
          extensions: [getFavouriteExtension(extensionId)] // will register the favourite extension
        }
      }
    </script>
    <script defer type="text/javascript" src="https://cdn.jsdelivr.net/npm/jmap-app-js@7_Kathmandu_HF3"></script>
  </body>
</html>

Dernière mise à jour

K2 Geospatial 2023