# 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](https://k2geospatial.github.io/jmap-app/latest/interfaces/jappextension.html).

The JMap NG App extension API documentation is available [here](https://k2geospatial.github.io/jmap-app/latest/interfaces/jappextension.html).

## 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](https://codepen.io/k2-dev/pen/RwxWozd?editors=1000)

```html
<!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:

```javascript
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.

<figure><img src="https://2224254070-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDUF1p2rT0hfE3QBlbbvE%2Fuploads%2FwstnH6bf7QeKJJLTleOR%2FCapture%20d%E2%80%99e%CC%81cran%2C%20le%202024-05-06%20a%CC%80%2013.50.23.png?alt=media&#x26;token=86af42ae-413c-4606-a374-eb98429795fd" alt="" width="375"><figcaption><p>A more comple example</p></figcaption></figure>

\
Try it out in [Codepen.io](https://codepen.io/k2-dev/pen/PoEPbVr)

```html
<!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>
```
