MapSimple

A MapSimple example

A map showing the users locationm, markers, and a popup that appears when you tap on them.

Data for multiple markers must be loaded using DataLoaderMulti (or if you only want to show only a single marker, through DataLoaderSingle).

See the Data Loading tutorial for an example of setting up a simple map.

See MapViewButton for an interface that provides filters and layers.


Developer docs

Props

Prop Type Default Description
clickTrigger*
mapBoxGLStyle*
tileLayer*
showPopups*
showControls*
height*
defaultLocation*
hideOnMapColumn*
markerTitleColumn*
markerLabelColumn*
customIconColumn*
secondaryPositionProperty*
markerPositionsColumn*
markerCheckedIconAsset*
markerIconAsset*
checkedProperty string "checked"
permissionNotification string "Die App hat keine Erlaubnis, ihre Position festzustellen. Unter Start > Einstellungen > FAQ finden Sie eine Anleitung, um die Erlaubnis für Ihr Gerät zu erteilen."
enableGeolocationHint string "Bitte aktivieren Sie ihren Standort."
mapId string "map"
nearestElementMode boolean false
inline boolean false
disableControls boolean false
singleElementContext boolean false
closeButtonLabel string "Schließen"

Slots

Name Default Props Fallback
button No
popup No

Source

see source code
<script>

  import { onMount, setContext, getContext, onDestroy } from 'svelte'
  import { get, writable } from 'svelte/store'
  import { fly } from 'svelte/transition';
  
  import { InterkitClient, util } from '../'
  import { executeTrigger } from '../actions'
  import { getShowDummyDataStore } from './dummyDataHelpers.js'

  import Button from './Button.svelte'
  import ButtonBar from './ButtonBar.svelte';
  import Icon from './Icon.svelte'
  import MapRenderer from './MapRenderer.svelte'
  import ContextProvider from './ContextProvider.svelte';
  
  export let markerIconAsset; // default asset to use
  export let markerCheckedIconAsset; // checked asset
  export let markerPositionsColumn; // where the markers are
  export let secondaryPositionProperty; // an optional elementProperty that gives an element a user specific position
  export let customIconColumn; // a custom mediafile as icon for each element
  export let markerLabelColumn; // a short custom string for the marker (eg "01")
  export let markerTitleColumn; // a short custom string to appear above the marker, outside the bubble (eg "Foo Station")
  export let hideOnMapColumn; // option on elements to hide on map
  export let checkedProperty = "checked" // what property to use for the checkmark
  
  export let defaultLocation; // where to center the map [lat, lng]
  export let permissionNotification = "Die App hat keine Erlaubnis, ihre Position festzustellen. Unter Start > Einstellungen > FAQ finden Sie eine Anleitung, um die Erlaubnis für Ihr Gerät zu erteilen.";
  export let enableGeolocationHint = "Bitte aktivieren Sie ihren Standort."
  export let height; // height of the container
  export let showControls; // true if we should show controls
  export let showPopups; // true if we should show popup on marker tap
  export let mapId = "map"; // id of the map
  export let nearestElementMode = false; // mode to show only the nearest element
  export let inline = false;
  export let disableControls = false;
  export let singleElementContext = false; // mode to retrieve element from context and show just that
  
  export let tileLayer // simple tileLayer, "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
  let activeTileLayer = tileLayer // this can be changed through MapViewButton
  
  export let mapBoxGLStyle // mapboxGL style, probably a URL like https://api.maptiler.com/maps/1234uuid/style.json?key=f0o. If null-ish or "interkit", default stadiamaps (non-mapboxGL) will be used.
  let activeGLStyle = mapBoxGLStyle // this can be changed through MapViewButton

  export let closeButtonLabel = "Schließen"
  export let clickTrigger;

  // set defaultJson if theming is on
  const themeStyleUrl = "theme/maptiler/style.json"
  let defaultGLStyle = false
  const config = get(InterkitClient.config)
  console.log("MapRenderer config", config)
  if (config && config.INTERKIT_APP_LOAD_THEME) {
    // check if themeStyleUrl returns a json
    fetch(themeStyleUrl)
      .then(response => response.json())
      .then(data => {
        console.log("MapRenderer theme style.json found")
        defaultGLStyle = themeStyleUrl
      })
      .catch(e => {
        console.error(themeStyleUrl + " not a valid json", e)
      })
  }

  // for new iOS only at this moment
  const deviceorientationRequestPermission = () => {
    if (typeof DeviceMotionEvent.requestPermission === 'function') {
      console.log('DME reqPerm')
      DeviceMotionEvent.requestPermission()
        .then(permState => {
          console.log('DME reqPerm then', permState)
          if (permState === 'granted') {
            console.log('DMQ reqPerm granted')
            // the event listener will pick up deviceorientation events now
          }
        })
        .catch(e => {
          console.error('permReq error', e)
        })
    }
  }

  let columnMap = {
    customIconColumn,
    markerLabelColumn,
    markerTitleColumn,
    hideOnMapColumn,
    markerPositionsColumn
  }

  const elementProperties = InterkitClient.getGlobalStore("elementProperties")
  const mapFocus = InterkitClient.getGlobalStore("mapFocus")
  
  // check if secondaryPositionProperty should be used for mapFocus
  let mapFocusProcessed
  const processMapFocus = (value) => {
    let processed = value;
    if(value && value?.key && secondaryPositionProperty 
      && $elementProperties?.[value?.key]?.[secondaryPositionProperty]) {
        processed = $elementProperties?.[value?.key]?.[secondaryPositionProperty]
      }
    selectedElement = value  
    return processed;
  }
  $: mapFocusProcessed = processMapFocus($mapFocus)

  const userPositionStore = InterkitClient.getGlobalStore("userPosition");
  
  let markerObjects;
  let markerData; 
  let selectedElement;
  let nearestElement;
  let singleElement;

  let elementsContext = getContext("elements");
  let elements = elementsContext?.elements;
  console.log("MapSimple, got elements from context", elements)

  let unsubElements; // unsubscribe method to this store
  let markerObjs; // where we store the objects
  
  // retrieve row from qr scanner and convert to object with the columns specified in map
  let qrContext = getContext("qr-scanner")
  if(qrContext?.targetElementObj) {
    singleElement = util.rowToObject(qrContext.targetElementObj.row, columnMap)
    console.log("singleElement", singleElement, columnMap)
  }

  // check if we should show only a single element specified through context
  const elementContext = getContext("element")
  $: {
    if(singleElementContext) {
      if($elementContext) {
        singleElement = util.rowToObject($elementContext, columnMap)
        console.log("singleElement", singleElement)
        updateMarkerData();
      }
    }
  }

  if(!elements && !singleElement) console.warn("Warning: MapSimple needs elements or QRScanner context to show markers");

  // setup dummy data
  function getRandomInRange(from, to, fixed) {
    return (Math.random() * (to - from) + from).toFixed(fixed) * 1;
    // .toFixed() returns string, so ' * 1' is a trick to convert to number
  }
  const showDummyData = getShowDummyDataStore()
  const dummyLocations = [
    ...Array(10).fill(null).map(_ => [getRandomInRange(-90, 90, 3), getRandomInRange(-180, 180, 3)]),
    [52.52083594391814, 13.409404500259926]
  ]
  const dummyData = dummyLocations
    .map(([lat, lng], k) => ({
      key: `${k}`,
      row: {
        key: `${k}`,
        values: {
          position: { lat, lng },
          markerTitle: `markerTitle ${k}`
        }
      }
    }))

  /*
  const dummyData = [
    ...Array(10).keys()].map((k) => {return {key: `${k}`, row: {key: `${k}`, values: {
      position: {
        lat: getRandomInRange(-90, 90, 3),
        lng: getRandomInRange(-180, 180, 3)
      },
      markerTitle: "markerTitle"
    }
  }}})
  */
  const dummyDataStore = writable(dummyData)
  if($showDummyData) {
    elements = dummyDataStore
    console.log("set elements to dummyData")
  }
  if($showDummyData) {
    columnMap.markerPositionsColumn = "elements/position"
    columnMap.markerTitleColumn = "elements/markerTitle"
  }
  // more dummy Data set in MapRenderer/createIconDivHTML

  // set up subscription
  const initDataSubs = async () => {
    if(elements) {
      // convert elements to objects with the columns we need
      unsubElements = elements.subscribe((data)=>{
        console.log("map elements data", data)
        markerObjs = data.map(e => util.rowToObject(e.row, columnMap));
        console.log("map elements markerObj", markerObjs)
        updateMarkerData();
      })
    }
  }

  const distanceSort = (a, b) => {
    return util.getDistance(a.markerPositionsColumn, $userPositionStore) - util.getDistance(b.markerPositionsColumn, $userPositionStore)
  }

  // a store for active map views changed in MapViewButton
  const mapViewState = InterkitClient.getGlobalStore("mapViewState-" + mapId)

  // update activeTileLayer and activeGLStyle if map layer changed
  const updateTileLayer = (layer) => {
    if(layer) {
      activeTileLayer = layer?.tilesUrlColumn
      activeGLStyle = layer?.mapBoxGLStyleColumn
    } else {
      activeTileLayer = tileLayer
      activeGLStyle = mapBoxGLStyle || defaultGLStyle
    }
  }
  $: {
    defaultGLStyle // trigger updateTileLayer when defaultGLStyle is set
    updateTileLayer($mapViewState?.activeLayers?.[0])
  }
  
  $: {
    selectedElement;
    $userPositionStore;
    $mapViewState;
    updateMarkerData();
  }
  
  // preprocess data for marker creation in map renderer
  const updateMarkerData = async () => {

    if(!markerObjs) {
      markerData = [];
      return
    }

    // start with the full set of data
    let selectedData = [...markerObjs];

    // filter data according to active map views
    const filterViews = $mapViewState?.activeFilters || []
    const layerViews = $mapViewState?.activeLayers?.filter(v => v.typeColumn == "layer+filter") || []
    const activeViews = filterViews.concat(layerViews) 

    if(activeViews.length && $mapViewState?.markerCategoryColumn) {
      
      // get the category column on the data row
      const markerCategoryColumn = $mapViewState?.markerCategoryColumn

      // get the keys of the active views
      const activeViewKeys = activeViews?.map(e => e.key)

      let filteredData = []
      // iterate over data and check if it references an active view
      for(let e of selectedData) {
        const referencedViewKeys = e?.row?.values?.[util.colKey(markerCategoryColumn)]?.rowKeys
        if(referencedViewKeys) {
          if(referencedViewKeys.some(k => activeViewKeys.includes(k))) {
            filteredData.push(e)
          }
        }
      }
      selectedData = filteredData      
    }

    // if singleElement mode is set, use only that
    if(singleElementContext && singleElement) {
      //console.log("updateMarkerData, using single Element", singleElement)
      selectedData = [singleElement]
    }

    // find nearest element
    if($userPositionStore) {
      let markerObjs_sorted = [...selectedData].filter(r => r.markerPositionsColumn).sort(distanceSort)
      if(markerObjs_sorted.length) {
        nearestElement = markerObjs_sorted[0]
        // if nearestElementMode is set and we have a position, show only nearest element
        if(nearestElementMode) {
          selectedData = [nearestElement]
        }
      }
    }

    // prepare data for marker production
    markerData = selectedData.map(r=> {return {
      location: (secondaryPositionProperty && $elementProperties?.[r.key]?.[secondaryPositionProperty]) ?
                  $elementProperties?.[r.key]?.[secondaryPositionProperty] 
                :
                (
                  (r.markerPositionsColumn?.lat && r.markerPositionsColumn?.lng) ?
                  r.markerPositionsColumn : undefined
                ),
      checked: $elementProperties?.[r.key]?.[checkedProperty] ? true : false, 
      selected: selectedElement?.key == r.key ? true : false,
      element: r
    }})

    //console.log("updateMarkerData", markerData, mapId, $elementProperties)
  }

  const markerClick = async (e) => {
    //console.log("marker clicked", e.target?.payload);
    
    if(showPopups) {
      selectedElement = {
        ...e.target?.payload?.elementRow,
        onPlay: () => {selectedElement = null}
      }
    }
  }

  const mapClick = () => {
    selectedElement = null;
  }

  onMount(async ()=>{
    await initDataSubs();      
    deviceorientationRequestPermission()
  })

  onDestroy(()=>{
    if(unsubElements)
      unsubElements()
  })

  // set context for buttons in buttons slot
  const buttonPayloadStore = writable(null)
  setContext("buttonBar", {
    buttonPayload: buttonPayloadStore
  });

  // update store whenever it changes
  $: buttonPayloadStore.set(selectedElement ? selectedElement : nearestElement?.row)

  const containerClick = () => {
    deviceorientationRequestPermission()
    if(clickTrigger) {
      executeTrigger(clickTrigger)
    }
  }

  
</script>

  <!-- svelte-ignore a11y-click-events-have-key-events -->
  <div
    class="MapSimple map-component-container"
    on:click={containerClick}
    class:inline="{inline}"
    class:MapSimple--inline="{inline}"
    >

    {#if selectedElement && $$slots.popup}
      <div class="MapSimple__MarkerPopup marker_popup" 
        class:active={selectedElement ? true : false}
        class:MapSimple__MarkerPopup--active={selectedElement ? true : false}
        in:fly="{{ y: 300, duration: 100, opacity: 1 }}"
      >
        <div class="MapSimple__MarkerPopupClose marker_popup_close">
          <Button type="secondary" on:click={mapClick} dummyNoText>
            <Icon type="Thin-Close"/>
            <span>{closeButtonLabel}</span>
          </Button>
        </div>
        <div class="MapSimple__MarkerPopupBackground marker_popup_background">
          {#if selectedElement}
            <ContextProvider 
              name="element" 
              value={selectedElement}
            >
              <slot name="popup"></slot>
            </ContextProvider>
          {/if}
        </div>
      </div>
    {/if}

    <MapRenderer
      {defaultLocation}
      {height}
      {showControls}
      {mapId}
      {markerData}
      {markerClick}
      {mapClick}
      {nearestElementMode}
      {nearestElement}
      {singleElement}
      {disableControls}
      tileLayer={activeTileLayer}
      mapBoxGLStyle={activeGLStyle}
      mapFocus={mapFocusProcessed}
      {permissionNotification}
      {enableGeolocationHint}
    />

    {#if $$slots.button}
      <div class="MapSimple__Buttons button-container">
        <ButtonBar hideHelpText>
          <slot name="button"></slot>
        </ButtonBar>
      </div>
    {/if}


  </div>

<style>

  .map-component-container {
    height: 100%;
    position: relative;
  }

  .map-component-container.inline {
    position: relative;
    overflow: hidden;
    height: auto;
    display: flex;
    flex-direction: row;
    min-width: 2.5rem;
    /* FIXME? doesn't exist any more
    font-size: var(--font-size-regular);
    */
  }

  /* need to be very cautious for iOS */
  .map-component-container.inline,
  .map-component-container.inline :global(.Map__Container),
  .map-component-container.inline :global(.map) {
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
  }

  :global(.marker-content-label) {
    font: var(--font-caption-bold);
    letter-spacing: var(--letter-spacing-caption-bold);
  }

  :global(div.marker-container.selected) {
    background-color: var(--color-background-highlight);
  }

  :global(div.marker-container.selected img) {
    filter: grayscale(1);
  }

  .marker_popup {
    position: absolute;
    bottom: calc(var(--outset-y) * 0.5rem);
    padding-left: calc(var(--outset-x) * 0.5rem);
    padding-right: calc(var(--outset-x) * 0.5rem);
    z-index: 2000;
    display: none;
    width: 100%;
    box-sizing: border-box;
  }

  .marker_popup_close {
    position: absolute;
    top: calc(-2.5rem - var(--outset-y) * 0.5rem);
    z-index: 10;
  }

  .marker_popup_background {
    position: relative;
    /*background-color: var(--color-background);
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);*/
    overflow: hidden;
  }

  .marker_popup.active {
    display: block; 
  }

  .button-container {
    position: absolute;
    z-index: 1000;
    bottom: calc(var(--outset-y) * 0.5rem);
    left: calc(var(--outset-x) * 0.5rem);
    right: calc(var(--outset-x) * 0.5rem);
  }

  :global(.Map__Button__Bar .Button) {
    margin-right: calc(var(--outset-x) * 0.5rem);
  }
  
</style>