Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CustomControl components #19

Merged
merged 17 commits into from
Nov 21, 2024
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ Everyone is welcomed to contribute to this project! There are many ways to suppo
- TODO: Add core contributors
- And [all contributors](https://github.com/MIERUNE/svelte-maplibre-gl/graphs/contributors)

Supported by [MIERUNE Inc.](https://www.mierune.co.jp/)

## Acknowledgements

This project `svelte-maplibre-gl` is inspired by the efforts and innovations of the following libraries:
Expand Down
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.5",
"version": "0.0.6",
"license": "(MIT OR Apache-2.0)",
"description": "Svelte library for using MapLibre GL JS as reactive components",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions src/content/examples/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
<li><a href="/examples/canvas-source">Canvas Source</a></li>
<li><a href="/examples/fullscreen">Fullscreen</a></li>
<li><a href="/examples/geolocate">Locate the User</a></li>
<li><a href="/examples/custom-control">Custom Control</a></li>
</ul>
88 changes: 88 additions & 0 deletions src/content/examples/custom-control/CustomControl.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import { HillshadeLayer, MapLibre, RasterDEMTileSource, Terrain, CustomControl } from 'svelte-maplibre-gl';
import maplibregl from 'maplibre-gl';
import Sun from 'lucide-svelte/icons/sun';
import Moon from 'lucide-svelte/icons/moon';
import ArrowUpLeft from 'lucide-svelte/icons/arrow-up-left';
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
import ArrowDownLeft from 'lucide-svelte/icons/arrow-down-left';
import ArrowDownRight from 'lucide-svelte/icons/arrow-down-right';
import { MyControl } from './MyControl.js';
ciscorn marked this conversation as resolved.
Show resolved Hide resolved

let isHillshadeVisible = $state(true);
let isTerrainVisible = $state(true);
let isDarkMode = $state(false);
const mapStyle = $derived(
isDarkMode
? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'
: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json'
);
let center = $state({ lng: 11.09085, lat: 47.3 });
let controlPosition: maplibregl.ControlPosition = $state('top-left');

const myControl = new MyControl({
toggleHillshade: () => {
isHillshadeVisible = !isHillshadeVisible;
return isHillshadeVisible;
},
toggleTerrain: () => {
isTerrainVisible = !isTerrainVisible;
return isTerrainVisible;
}
});
</script>

<MapLibre class="h-[50vh] min-h-[200px]" style={mapStyle} zoom={12} pitch={40} maxPitch={85} bind:center>
<!-- inject IControl (useful for plugin) -->
<CustomControl position="top-left" control={myControl} />

<!-- Control / Group / Icon -->
<CustomControl position="bottom-left">
<button onclick={() => (isDarkMode = !isDarkMode)} class="grid place-items-center text-gray-900">
{#if isDarkMode}
<Moon class="w-5" />
{:else}
<Sun class="w-5" />
{/if}
</button>
ciscorn marked this conversation as resolved.
Show resolved Hide resolved
</CustomControl>

<!-- Group -->
<CustomControl position={controlPosition} class="text-gray-900">
<button class="place-items-center" onclick={() => (controlPosition = 'top-left')}
><ArrowUpLeft class="w-5" /></button
>
<button class="place-items-center" onclick={() => (controlPosition = 'top-right')}
><ArrowUpRight class="w-5" /></button
>
<button class="place-items-center" onclick={() => (controlPosition = 'bottom-right')}
><ArrowDownRight class="w-5" /></button
>
<button class="place-items-center" onclick={() => (controlPosition = 'bottom-left')}
><ArrowDownLeft class="w-5" /></button
>
</CustomControl>

<!-- Control / Group / any svelte elements -->
<CustomControl position="top-right">
<div class="p-2 text-yellow-700">Arbitrary HTML</div>
<div class="border-t border-t-[#ddd] p-2 text-center text-yellow-700">
({center.lat.toFixed(4)}, {center.lat.toFixed(4)})
ciscorn marked this conversation as resolved.
Show resolved Hide resolved
</div>
</CustomControl>

<RasterDEMTileSource
id="terrain"
tiles={['https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png']}
minzoom={0}
maxzoom={12}
attribution="<a href='https://earth.jaxa.jp/en/data/policy/'>AW3D30 (JAXA)</a>"
>
Kanahiro marked this conversation as resolved.
Show resolved Hide resolved
{#if isTerrainVisible}
<Terrain />
{/if}
{#if isHillshadeVisible}
<HillshadeLayer />
{/if}
</RasterDEMTileSource>
</MapLibre>
58 changes: 58 additions & 0 deletions src/content/examples/custom-control/MyControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
interface MyControlConstructorOptions {
toggleTerrain: () => boolean;
toggleHillshade: () => boolean;
}

class MyControl implements maplibregl.IControl {
private _container: HTMLElement | undefined;
private _toggleTerrain: () => boolean;
private _toggleHillshade: () => boolean;

constructor(options: MyControlConstructorOptions) {
this._toggleTerrain = options.toggleTerrain;
this._toggleHillshade = options.toggleHillshade;
}

onAdd() {
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group p-2 rounded flex w-[240px] gap-x-2';

const toggleTerrain = document.createElement('button');
toggleTerrain.textContent = 'Disable Terrain';
toggleTerrain.type = 'button';
toggleTerrain.style.backgroundColor = 'red';
toggleTerrain.style.color = 'white';
toggleTerrain.style.width = '50%';
toggleTerrain.style.height = '100%';
toggleTerrain.style.borderRadius = '0.25rem';
toggleTerrain.addEventListener('click', () => {
const newState = this._toggleTerrain();
toggleTerrain.textContent = newState ? 'Disable Terrain' : 'Enable Terrain';
});

const toggleHillshade = document.createElement('button');
toggleHillshade.textContent = 'Disable Hillshade';
toggleHillshade.type = 'button';
toggleHillshade.style.backgroundColor = 'blue';
toggleHillshade.style.color = 'white';
toggleHillshade.style.height = '100%';
toggleHillshade.style.width = '50%';
toggleHillshade.style.borderRadius = '0.25rem';
toggleHillshade.addEventListener('click', () => {
const newState = this._toggleHillshade();
toggleHillshade.textContent = newState ? 'Disable Hillshade' : 'Enable Hillshade';
});

this._container.appendChild(toggleTerrain);
this._container.appendChild(toggleHillshade);
return this._container!;
}
ciscorn marked this conversation as resolved.
Show resolved Hide resolved

onRemove() {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
}
ciscorn marked this conversation as resolved.
Show resolved Hide resolved
}

export { MyControl };
14 changes: 14 additions & 0 deletions src/content/examples/custom-control/content.svelte.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Custom Control
description: Custom Control allows to easily create user defined controls.
---

<script lang="ts">
import CustomControl from "./CustomControl.svelte";
import demoRaw from "./CustomControl.svelte?raw";
import CodeBlock from "../../CodeBlock.svelte";
</script>

<CustomControl />

<CodeBlock content={demoRaw} />
52 changes: 52 additions & 0 deletions src/lib/maplibre/controls/CustomControl.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
// https://maplibre.org/maplibre-gl-js/docs/API/interfaces/IControl/

import type { Snippet } from 'svelte';
import maplibregl from 'maplibre-gl';
import { getMapContext } from '../contexts.svelte.js';

interface Props {
position?: maplibregl.ControlPosition;
control?: maplibregl.IControl;
group?: boolean;
class?: string;
children?: Snippet;
}
let { position, control: givenControl, class: className, group = true, children }: Props = $props();
if (!givenControl && !children) throw new Error('You must provide either control or children.');

const mapCtx = getMapContext();
if (!mapCtx.map) throw new Error('Map instance is not initialized.');

let el: HTMLDivElement | undefined = $state();

let control = $derived.by(() => {
if (givenControl) {
return givenControl;
}

return {
onAdd: () => {
return el!;
},
onRemove: () => {
el?.parentNode?.removeChild(el);
}
};
});
ciscorn marked this conversation as resolved.
Show resolved Hide resolved

$effect(() => {
if (control) {
mapCtx.map?.addControl(control, position);
}
return () => {
control && mapCtx.map?.removeControl(control);
};
});
Comment on lines +38 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix linting error and improve cleanup logic.

The effect hook has a linting error and could benefit from more explicit cleanup.

Apply this improvement:

 $effect(() => {
   if (control) {
     mapCtx.map?.addControl(control, position);
   }
   return () => {
-    control && mapCtx.map?.removeControl(control);
+    if (control && mapCtx.map) {
+      mapCtx.map.removeControl(control);
+    }
   };
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$effect(() => {
if (control) {
mapCtx.map?.addControl(control, position);
}
return () => {
control && mapCtx.map?.removeControl(control);
};
});
$effect(() => {
if (control) {
mapCtx.map?.addControl(control, position);
}
return () => {
if (control && mapCtx.map) {
mapCtx.map.removeControl(control);
}
};
});
🧰 Tools
🪛 eslint

[error] 43-43: Expected an assignment or function call and instead saw an expression.

(@typescript-eslint/no-unused-expressions)

</script>

{#if !givenControl}
<div bind:this={el} class={`maplibregl-ctrl ${className}`} class:maplibregl-ctrl-group={group}>
{@render children?.()}
</div>
{/if}
1 change: 1 addition & 0 deletions src/lib/maplibre/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export { default as FullScreenControl } from './controls/FullScreenControl.svelt
export { default as TerrainControl } from './controls/TerrainControl.svelte';
export { default as ScaleControl } from './controls/ScaleControl.svelte';
export { default as LogoControl } from './controls/LogoControl.svelte';
export { default as CustomControl } from './controls/CustomControl.svelte';
export { default as Hash } from './controls/Hash.svelte';