mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-30 23:30:04 +00:00
beginning of map layer control
This commit is contained in:
24
website/package-lock.json
generated
24
website/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
85
website/src/lib/assets/layers.ts
Normal file
85
website/src/lib/assets/layers.ts
Normal 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: '© <a href="https://www.opentopomap.org" target="_blank">OpenTopoMap</a> © <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: '© <a href="https://wiki.openstreetmap.org/wiki/Hiking/mri" target="_blank">sly</a> © <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: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <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: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
},
|
||||
};
|
63
website/src/lib/components/LayerControl.svelte
Normal file
63
website/src/lib/components/LayerControl.svelte
Normal 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>
|
@@ -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">
|
||||
|
@@ -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>
|
20
website/src/lib/components/custom-control/CustomControl.ts
Normal file
20
website/src/lib/components/custom-control/CustomControl.ts
Normal 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;
|
||||
}
|
||||
}
|
35
website/src/lib/components/ui/checkbox/checkbox.svelte
Normal file
35
website/src/lib/components/ui/checkbox/checkbox.svelte
Normal 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>
|
6
website/src/lib/components/ui/checkbox/index.ts
Normal file
6
website/src/lib/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./checkbox.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
7
website/src/lib/components/ui/label/index.ts
Normal file
7
website/src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
21
website/src/lib/components/ui/label/label.svelte
Normal file
21
website/src/lib/components/ui/label/label.svelte
Normal 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>
|
15
website/src/lib/components/ui/radio-group/index.ts
Normal file
15
website/src/lib/components/ui/radio-group/index.ts
Normal 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,
|
||||
};
|
@@ -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>
|
14
website/src/lib/components/ui/radio-group/radio-group.svelte
Normal file
14
website/src/lib/components/ui/radio-group/radio-group.svelte
Normal 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>
|
Reference in New Issue
Block a user