Svelte Maplibre

Overlapping Layer Events

Show popup on
Allow popup on
Last Event for red layer: NoneLast Event for green layer: NoneLast Event for blue layer: None
<script lang="ts">
  import MapLibre from 'svelte-maplibre/MapLibre.svelte';
  import CodeSample from '$site/CodeSample.svelte';
  import code from './+page.svelte?raw';
  import GeoJson from 'svelte-maplibre/GeoJSON.svelte';
  import CircleLayer from 'svelte-maplibre/CircleLayer.svelte';
  import { hoverStateFilter } from 'svelte-maplibre/filters';
  import Popup from 'svelte-maplibre/Popup.svelte';

  type PointFeature = GeoJSON.Feature<GeoJSON.Point, { radius: number; color: string }>;

  function randomCircle(color: string): PointFeature {
    const lng = Math.random() * 360 - 180;
    const lat = Math.random() * 180 - 90;
    const radius = Math.random() * 50;

    return {
      type: 'Feature',
      properties: {
        radius,
        color,
      },
      geometry: {
        type: 'Point',
        coordinates: [lng, lat],
      },
    };
  }

  const layer1 = Array.from({ length: 20 }, () => randomCircle('red'));
  const layer2 = Array.from({ length: 20 }, () => randomCircle('green'));
  const layer3 = Array.from({ length: 20 }, () => randomCircle('blue'));

  const layers = [
    { data: layer1, color: 'red', hoverCursor: 'help' },
    { data: layer2, color: 'green', hoverCursor: '' },
    { data: layer3, color: 'blue', hoverCursor: 'not-allowed' },
  ];

  const lastEvent: (GeoJSON.Feature<GeoJSON.Point> | undefined)[] = $state([]);

  function labelFeature(f: GeoJSON.Feature<GeoJSON.Point> | undefined) {
    if (!f) {
      return 'None';
    }

    return f.geometry.coordinates.map((c) => c.toFixed(4)).join(' ,');
  }

  let eventsIfTopMost = $state(true);
  let openIfTopMost = $state(true);
  let openOn: 'click' | 'dblclick' | 'contextmenu' | 'hover' = $state('hover');
  let allowOn: 'all' | 'red' | 'green' | 'blue' = $state('all');
</script>

<div class="mx-auto mb-2 flex flex-col items-start gap-1">
  <label>
    <input type="checkbox" bind:checked={eventsIfTopMost} />
    When layers overlap, only fire events for top-most layer
  </label>

  <label>
    <input type="checkbox" bind:checked={openIfTopMost} />
    When layers overlap, only activate the popup for top-most layer
  </label>

  <fieldset class="flex gap-x-4">
    <legend>Show popup on</legend>
    <label><input type="radio" bind:group={openOn} value="hover" /> Hover</label>
    <label><input type="radio" bind:group={openOn} value="click" /> Click</label>
    <label><input type="radio" bind:group={openOn} value="dblclick" /> Double Click</label>
    <label
      ><input type="radio" bind:group={openOn} value="contextmenu" /> Context Menu (right-click)</label
    >
  </fieldset>

  <fieldset class="flex gap-4">
    <legend>Allow popup on</legend>
    <label><input type="radio" bind:group={allowOn} value="all" /> All Colors</label>
    <label><input type="radio" bind:group={allowOn} value="red" /> Only Red</label>
    <label><input type="radio" bind:group={allowOn} value="green" /> Only Green</label>
    <label><input type="radio" bind:group={allowOn} value="blue" /> Only Blue</label>
  </fieldset>

  <div class="grid grid-cols-[auto_1fr] gap-x-2">
    {#each layers as layer, i}
      <span class="flex-none">Last Event for {layer.color} layer: </span>
      <span class="w-64">{labelFeature(lastEvent[i])}</span>
    {/each}
  </div>
</div>

<MapLibre
  style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
  class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
  standardControls
  zoomOnDoubleClick={openOn !== 'dblclick'}
>
  {#each layers as { data, color, hoverCursor }, i}
    <GeoJson id="layer{i + 1}" data={{ type: 'FeatureCollection', features: data }} generateId>
      <CircleLayer
        {eventsIfTopMost}
        manageHoverState
        {hoverCursor}
        paint={{
          'circle-color': color,
          'circle-radius': ['get', 'radius'],
          'circle-opacity': hoverStateFilter(1.0, 0.5),
        }}
        onclick={(e) => {
          lastEvent[i] = e.features?.[0] as PointFeature;
        }}
        onmouseleave={(e) => {
          lastEvent[i] = undefined;
        }}
        onmousemove={(e) => {
          lastEvent[i] = e.features?.[0] as PointFeature;
        }}
      >
        <Popup
          {openOn}
          {openIfTopMost}
          canOpen={(features) => allowOn === 'all' || features?.[0]?.properties?.color === allowOn}
        >
          {#snippet children({ features })}
            <div style:background={color} style:color="white">
              <p>{features?.length} features from {color} layer</p>
              {#if color == 'red'}
                <p>extra padding for lowest red layer</p>
                <p>extra padding for lowest red layer</p>
              {/if}
              {#if color == 'green'}
                <p>extra padding for middle green layer</p>
              {/if}
            </div>
          {/snippet}
        </Popup>
      </CircleLayer>
    </GeoJson>
  {/each}
</MapLibre>

Back to Examples

Github