beginning of map layer control

This commit is contained in:
vcoppe
2024-04-11 17:18:21 +02:00
parent b5a9fa3218
commit 75ef69ab42
14 changed files with 356 additions and 2 deletions

View File

@@ -11,6 +11,8 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/mapbox-gl": "^3.1.0",
"bits-ui": "^0.21.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",
@@ -1370,6 +1372,11 @@
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
"dev": true
},
"node_modules/@types/geojson": {
"version": "7946.0.14",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
@@ -1389,6 +1396,23 @@
"@types/node": "*"
}
},
"node_modules/@types/mapbox__mapbox-gl-geocoder": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-gl-geocoder/-/mapbox__mapbox-gl-geocoder-5.0.0.tgz",
"integrity": "sha512-eGBWdFiP2QgmwndPyhwK6eBeOfyB8vRscp2C6Acqasx5dH8FvTo/VgXWCrCKFR3zkWek/H4w4/CwmBFOs7OLBA==",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox-gl": "*"
}
},
"node_modules/@types/mapbox-gl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.1.0.tgz",
"integrity": "sha512-hI6cQDjw1bkJw7MC/eHMqq5TWUamLwsujnUUeiIX2KDRjxRNSYMjnHz07+LATz9I9XIsKumOtUz4gRYnZOJ/FA==",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",

View File

@@ -40,6 +40,8 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/mapbox-gl": "^3.1.0",
"bits-ui": "^0.21.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",

View File

@@ -0,0 +1,85 @@
import { type AnySourceData, type Style } from 'mapbox-gl';
export const basemaps: { [key: string]: string | Style; } = {
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
mapboxSatellite: 'mapbox://styles/mapbox/satellite-v9',
openStreetMap: {
version: 8,
sources: {
openStreetMap: {
type: 'raster',
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: 'Map tiles by <a target="_top" rel="noopener" href="https://tile.openstreetmap.org/">OpenStreetMap tile servers</a>, under the <a target="_top" rel="noopener" href="https://operations.osmfoundation.org/policies/tiles/">tile usage policy</a>. Data by <a target="_top" rel="noopener" href="http://openstreetmap.org">OpenStreetMap</a>'
}
},
layers: [{
id: 'openStreetMap',
type: 'raster',
source: 'openStreetMap',
}],
},
openTopoMap: {
version: 8,
sources: {
openTopoMap: {
type: 'raster',
tiles: ['https://tile.opentopomap.org/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://www.opentopomap.org" target="_blank">OpenTopoMap</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
}
},
layers: [{
id: 'openTopoMap',
type: 'raster',
source: 'openTopoMap',
}],
},
openHikingMap: {
version: 8,
sources: {
openHikingMap: {
type: 'raster',
tiles: ['https://maps.refuges.info/hiking/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://wiki.openstreetmap.org/wiki/Hiking/mri" target="_blank">sly</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
}
},
layers: [{
id: 'openHikingMap',
type: 'raster',
source: 'openHikingMap',
}],
},
cyclOSM: {
version: 8,
sources: {
cyclOSM: {
type: 'raster',
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
},
layers: [{
id: 'cyclOSM',
type: 'raster',
source: 'cyclOSM',
}],
},
linz: 'https://basemaps.linz.govt.nz/v1/tiles/topographic/EPSG:3857/style/topographic.json?api=d01fbtg0ar23gctac5m0jgyy2ds'
};
export const overlays: { [key: string]: AnySourceData; } = {
cyclOSMlite: {
type: 'raster',
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
},
};

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import mapboxgl from 'mapbox-gl';
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import Fa from 'svelte-fa';
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons';
import { Label } from '$lib/components/ui/label';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Checkbox } from '$lib/components/ui/checkbox';
import { basemaps, overlays } from '$lib/assets/layers';
export let map: mapboxgl.Map | null;
$: if (map) {
map?.setStyle(basemaps['mapboxOutdoors']);
}
</script>
<CustomControl {map} class="group">
<div class="flex flex-row justify-center items-center w-[29px] h-[29px] group-hover:hidden">
<Fa icon={faLayerGroup} size="1.4x" />
</div>
<div class="hidden group-hover:block p-2">
<RadioGroup.Root
value="mapboxOutdoors"
onValueChange={(id) => {
map.setStyle(basemaps[id]);
}}
>
{#each Object.keys(basemaps) as id}
<div class="flex items-center space-x-2">
<RadioGroup.Item value={id} {id} />
<Label for={id}>{id}</Label>
</div>
{/each}
</RadioGroup.Root>
<div>
{#each Object.keys(overlays) as id}
<Checkbox
{id}
onCheckedChange={(checked) => {
console.log('onCheckedChange', map?.isStyleLoaded());
if (checked) {
if (!map.getSource(id)) {
map.addSource(id, overlays[id]);
}
map.addLayer({
id,
type: overlays[id].type === 'raster' ? 'raster' : 'line',
source: id
});
} else {
map.removeLayer(id);
}
}}
/>
<Label for={id}>{id}</Label>
{/each}
</div>
</div>
</CustomControl>

View File

@@ -6,6 +6,7 @@
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import LayerControl from './LayerControl.svelte';
mapboxgl.accessToken =
'pk.eyJ1IjoiZ3B4c3R1ZGlvIiwiYSI6ImNrdHVoM2pjNTBodmUycG1yZTNwcnJ3MzkifQ.YZnNs9s9oCQPzoXAWs_SLg';
@@ -17,7 +18,7 @@
onMount(() => {
map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/outdoors-v12',
style: { version: 8, sources: {}, layers: [] },
projection: 'mercator',
hash: true,
language: 'auto',
@@ -57,10 +58,18 @@
})
);
});
onDestroy(() => {
if (map) {
map.remove();
}
});
</script>
<div {...$$restProps}>
<div id="map" class="h-full"></div>
<div id="map" class="h-full">
<LayerControl {map} />
</div>
</div>
<style lang="postcss">

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import CustomControl from './CustomControl';
import mapboxgl from 'mapbox-gl';
export let map: mapboxgl.Map | null;
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
let container: HTMLDivElement | null = null;
$: if (map && container) {
if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left');
container.classList.remove('hidden');
let control = new CustomControl(container);
map.addControl(control, position);
}
</script>
<div
bind:this={container}
class="{$$props.class ||
''} clear-both translate-0 m-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
>
<slot />
</div>

View File

@@ -0,0 +1,20 @@
import { type Map, type IControl } from 'mapbox-gl';
export default class CustomControl implements IControl {
_map: Map | undefined;
_container: HTMLElement;
constructor(container: HTMLElement) {
this._container = container;
}
onAdd(map: Map): HTMLElement {
this._map = map;
return this._container;
}
onRemove() {
this._container?.parentNode?.removeChild(this._container);
this._map = undefined;
}
}

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = false;
export { className as class };
</script>
<CheckboxPrimitive.Root
class={cn(
"peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50",
className
)}
bind:checked
{...$$restProps}
on:click
>
<CheckboxPrimitive.Indicator
class={cn("flex h-4 w-4 items-center justify-center text-current")}
let:isChecked
let:isIndeterminate
>
{#if isChecked}
<Check class="h-3.5 w-3.5" />
{:else if isIndeterminate}
<Minus class="h-3.5 w-3.5" />
{/if}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>

View File

@@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

View File

@@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

View File

@@ -0,0 +1,15 @@
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import Root from "./radio-group.svelte";
import Item from "./radio-group-item.svelte";
const Input = RadioGroupPrimitive.Input;
export {
Root,
Input,
Item,
//
Root as RadioGroup,
Input as RadioGroupInput,
Item as RadioGroupItem,
};

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<RadioGroupPrimitive.Item
{value}
class={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
on:click
>
<div class="flex items-center justify-center">
<RadioGroupPrimitive.ItemIndicator>
<Circle class="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.ItemIndicator>
</div>
</RadioGroupPrimitive.Item>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = RadioGroupPrimitive.Props;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<RadioGroupPrimitive.Root bind:value class={cn("grid gap-2", className)} {...$$restProps}>
<slot />
</RadioGroupPrimitive.Root>