Skip to content

Commit

Permalink
Merge pull request #48 from MIERUNE/query-features
Browse files Browse the repository at this point in the history
Declarative components for querySourceFeatures and queryRenderedFeatures
  • Loading branch information
ciscorn committed Dec 2, 2024
2 parents 834a834 + 3e84c98 commit 23995bf
Show file tree
Hide file tree
Showing 18 changed files with 342 additions and 17 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svelte-maplibre-gl",
"version": "0.0.10",
"version": "0.0.11",
"license": "(MIT OR Apache-2.0)",
"description": "Svelte library for using MapLibre GL JS as reactive components",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/content/CodeBlock.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
content: counter(step);
counter-increment: step;
width: 1em;
margin-right: 1em;
margin-right: 2em;
display: inline-block;
text-align: right;
color: rgba(115, 138, 148, 0.5);
color: rgba(115, 131, 148, 0.5);
}
</style>
12 changes: 9 additions & 3 deletions src/content/examples/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
'/examples/limit-interaction': 'Limit Map Interactions',
'/examples/animate-images': 'Animate a Series of Images',
'/examples/video-on-a-map': 'Video on a Map',
'/examples/image-loader': 'Load Images from URLs',
'/examples/fullscreen': 'Fullscreen',
'/examples/geolocate': 'Locate the User',
'/examples/canvas-source': 'Canvas Source'
'/examples/geolocate': 'Locate the User'
}
},
{
title: 'Utilities',
items: {
'/examples/image-loader': 'Load Images from URLs',
'/examples/query-features': 'Query Features'
}
},
{
Expand All @@ -31,6 +36,7 @@
items: {
'/examples/custom-control': 'Custom Control',
'/examples/custom-protocol': 'Custom Protocols',
'/examples/canvas-source': 'Canvas Source',
'/examples/custom-layer': 'Custom Layer',
'/examples/dynamic-image': 'Dynamic Image',
'/examples/threejs-model': '3D model with three.js'
Expand Down
4 changes: 2 additions & 2 deletions src/content/examples/pmtiles/PMTiles.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<!-- Use custom protocols -->
<MapLibre
class="h-[55vh] min-h-[200px]"
zoom={3}
center={[138.0, 36.5]}
zoom={10}
center={[12.484151635086198, 41.8960910478323]}
style={{
version: 8,
glyphs: 'https://tile.openstreetmap.jp/fonts/{fontstack}/{range}.pbf',
Expand Down
81 changes: 81 additions & 0 deletions src/content/examples/query-features/Query.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script lang="ts">
import {
MapLibre,
GeoJSONSource,
CircleLayer,
Marker,
GlobeControl,
QuerySourceFeatures,
QueryRenderedFeatures
} from 'svelte-maplibre-gl';
import maplibregl from 'maplibre-gl';
import * as Tabs from '$lib/components/ui/tabs/index.js';
let map: maplibregl.Map | undefined = $state();
let features: maplibregl.MapGeoJSONFeature[] = $state.raw([]);
let mode: 'source' | 'rendered' = $state('source');
</script>

<div class="flex h-[55vh] min-h-[300px] gap-x-3">
<MapLibre
bind:map
class="h-[55vh] min-h-[300px] grow"
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
zoom={3}
center={{ lng: 120, lat: 20 }}
>
<GlobeControl />
<GeoJSONSource
id="earthquakes"
data={'https://maplibre.org/maplibre-gl-js/docs/assets/significant-earthquakes-2015.geojson'}
promoteId="ids"
>
{#if mode == 'source'}
<!-- map.querySourceFeatures() -->
<QuerySourceFeatures bind:features>
{#snippet children(feature: maplibregl.MapGeoJSONFeature)}
{#if feature.geometry.type === 'Point'}
<Marker lnglat={feature.geometry.coordinates as [number, number]} />
{/if}
{/snippet}
</QuerySourceFeatures>
{/if}
<CircleLayer paint={{ 'circle-color': 'red', 'circle-radius': 4 }}>
{#if mode == 'rendered'}
<!-- map.queryRenderedFeatures() -->
<QueryRenderedFeatures bind:features>
{#snippet children(feature: maplibregl.MapGeoJSONFeature)}
{#if feature.geometry.type === 'Point'}
<Marker lnglat={feature.geometry.coordinates as [number, number]} />
{/if}
{/snippet}
</QueryRenderedFeatures>
{/if}
</CircleLayer>
</GeoJSONSource>
</MapLibre>

<!-- List of earthquakes -->
<div class="relative basis-[14em]">
<Tabs.Root bind:value={mode} class="flex h-full flex-col">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="source">Source</Tabs.Trigger>
<Tabs.Trigger value="rendered">Rendered</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="source" class="min-h-0 shrink overflow-scroll">
<ul class="m-0 ml-4 overflow-scroll px-3 text-xs">
{#each features as feature}
<li>{feature.properties.title}</li>
{/each}
</ul>
</Tabs.Content>
<Tabs.Content value="rendered" class="min-h-0 shrink overflow-scroll">
<ul class="m-0 ml-4 overflow-scroll px-3 text-xs">
{#each features as feature}
<li>{feature.properties.title}</li>
{/each}
</ul>
</Tabs.Content>
</Tabs.Root>
</div>
</div>
15 changes: 15 additions & 0 deletions src/content/examples/query-features/content.svelte.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Query Features
description: Query source and rendered features to place markers and display a list.
---

<script lang="ts">
import Demo from "./Query.svelte";
import demoRaw from "./Query.svelte?raw";
import CodeBlock from "../../CodeBlock.svelte";
let { shiki } = $props();
</script>

<Demo />

<CodeBlock content={demoRaw} {shiki} />
18 changes: 18 additions & 0 deletions src/lib/components/ui/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Tabs as TabsPrimitive } from "bits-ui";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";

const Root = TabsPrimitive.Root;

export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};
19 changes: 19 additions & 0 deletions src/lib/components/ui/tabs/tabs-content.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>

<TabsPrimitive.Content
bind:ref
class={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className
)}
{...restProps}
/>
19 changes: 19 additions & 0 deletions src/lib/components/ui/tabs/tabs-list.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>

<TabsPrimitive.List
bind:ref
class={cn(
"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
className
)}
{...restProps}
/>
19 changes: 19 additions & 0 deletions src/lib/components/ui/tabs/tabs-trigger.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>

<TabsPrimitive.Trigger
bind:ref
class={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
className
)}
{...restProps}
/>
21 changes: 19 additions & 2 deletions src/lib/maplibre/contexts.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { setContext, getContext } from 'svelte';

const MAP_CONTEXT_KEY = Symbol('MapLibre map context');
const SOURCE_CONTEXT_KEY = Symbol('MapLibre source context');
const LAYER_CONTEXT_KEY = Symbol('MapLibre layer context');
const MARKER_CONTEXT_KEY = Symbol('MapLibre marker context');

// https://svelte.dev/docs/svelte/$state#Classes
Expand Down Expand Up @@ -164,7 +165,7 @@ export function prepareMapContext(): MapContext {

export function getMapContext(): MapContext {
const mapCtx = getContext<MapContext>(MAP_CONTEXT_KEY);
if (!mapCtx) throw new Error('Component must be used inside MapLibre component');
if (!mapCtx) throw new Error('Map context not found');
return mapCtx;
}

Expand All @@ -182,10 +183,26 @@ export function prepareSourceContext(): SourceContext {

export function getSourceContext(): SourceContext {
const sourceCtx = getContext<SourceContext>(SOURCE_CONTEXT_KEY);
if (!sourceCtx || !sourceCtx.id) throw new Error('Must be used inside map Source context');
if (!sourceCtx || !sourceCtx.id) throw new Error('Source context not found');
return sourceCtx;
}

class LayerContext {
id: string = $state('');
}

export function prepareLayerContext(): LayerContext {
const layerCtx = new LayerContext();
setContext(LAYER_CONTEXT_KEY, layerCtx);
return layerCtx;
}

export function getLayerContext(): LayerContext | null {
const layerCtx = getContext<LayerContext>(LAYER_CONTEXT_KEY);
if (!layerCtx || !layerCtx.id) throw new Error('Layer context not found');
return layerCtx;
}

class MarkerContext {
marker: Marker | null = $state.raw(null);
}
Expand Down
3 changes: 1 addition & 2 deletions src/lib/maplibre/extensions/DeckGLOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@
function reactiveProp(name: keyof MapboxOverlayProps, value: unknown) {
if (!firstRun) {
// @ts-expect-error: awaitingChanges is a MapboxOverlayProps object
pendingChanges[name] = value;
pendingChanges[name] = $state.snapshot(value);
untrack(() => (changeTrigger += 1));
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/maplibre/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export { default as Hash } from './controls/Hash.svelte';

// utilities
export { default as ImageLoader } from './utilities/ImageLoader.svelte';
export { default as QuerySourceFeatures } from './utilities/QuerySourceFeatures.svelte';
export { default as QueryRenderedFeatures } from './utilities/QueryRenderedFeatures.svelte';

// extensions
export { default as PMTilesProtocol } from './extensions/PMTilesProtocol.svelte';
Expand Down
6 changes: 4 additions & 2 deletions src/lib/maplibre/layers/RawLayer.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, type Snippet } from 'svelte';
import maplibregl from 'maplibre-gl';
import { getMapContext, getSourceContext } from '../contexts.svelte.js';
import { getMapContext, getSourceContext, prepareLayerContext } from '../contexts.svelte.js';
import { generateLayerID, resetLayerEventListener } from '../utils.js';
import type { MapLayerEventProps } from './common.js';
Expand Down Expand Up @@ -51,6 +51,8 @@
if (!mapCtx.map) throw new Error('Map instance is not initialized.');
const id = _id ?? generateLayerID();
const layerCtx = prepareLayerContext();
layerCtx.id = id;
const addLayerObj = {
id,
Expand Down Expand Up @@ -154,7 +156,7 @@
filter;
if (!firstRun) {
mapCtx.waitForStyleLoaded((map) => {
map.setFilter(id, filter);
map.setFilter(id, $state.snapshot(filter) as maplibregl.FilterSpecification);
});
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/lib/maplibre/utilities/ImageLoader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
// Update image options if necessary
const image = mapCtx.map?.getImage(id);
if (image) {
const options = Array.isArray(src) ? src[1] : {};
const _src = $state.snapshot(src) as string | [string, Partial<maplibregl.StyleImageMetadata>];
const options = Array.isArray(_src) ? _src[1] : {};
let changed = false;
if (image.pixelRatio !== (options.pixelRatio ?? 1)) {
image.pixelRatio = options.pixelRatio ?? 1;
Expand Down
59 changes: 59 additions & 0 deletions src/lib/maplibre/utilities/QueryRenderedFeatures.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import { onDestroy, type Snippet } from 'svelte';
import maplibregl from 'maplibre-gl';
import { getLayerContext, getMapContext } from '../contexts.svelte.js';
interface Props extends maplibregl.QueryRenderedFeaturesOptions {
map?: maplibregl.Map;
geometry?: maplibregl.PointLike | [maplibregl.PointLike, maplibregl.PointLike];
features?: maplibregl.MapGeoJSONFeature[];
children?: Snippet<[maplibregl.MapGeoJSONFeature]>;
}
let { map: givenMap, geometry, features = $bindable([]), children, layers, ...options }: Props = $props();
features = [];
let trigger = $state(0);
let map = $derived(givenMap || getMapContext().map);
$effect(() => {
map?.on('render', update);
return () => {
map?.off('render', update);
};
});
function update() {
trigger++;
}
$effect(() => {
trigger;
if (!map) {
features = [];
return;
}
let _options = {
layers: layers || [getLayerContext()?.id || ''],
...options
};
let _geometry = geometry;
let queriedFeature = _geometry
? map.queryRenderedFeatures(_geometry, _options)
: map.queryRenderedFeatures(_options);
// sort
queriedFeature.sort((a, b) => {
return String(a.id).localeCompare(String(b.id));
});
features = queriedFeature;
});
onDestroy(() => {
features = [];
});
</script>

{#if children}{#each features as feature}{@render children(feature)}{/each}{/if}
Loading

0 comments on commit 23995bf

Please sign in to comment.