adds terrain to map

This commit is contained in:
Christian Beutel
2024-12-02 00:14:45 +01:00
parent b1d163a0a4
commit d9f7ed12f2
10 changed files with 164 additions and 39 deletions

View File

@@ -0,0 +1,54 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// add
new_terrain := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "mvxf9llv",
"name": "terrain",
"type": "url",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": [],
"onlyDomains": []
}
}`), new_terrain); err != nil {
return err
}
collection.Schema.AddField(new_terrain)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
// remove
collection.Schema.RemoveField("mvxf9llv")
return dao.SaveCollection(collection)
})
}

View File

@@ -17,6 +17,7 @@
import { createEventDispatcher, onDestroy, onMount } from "svelte"; import { createEventDispatcher, onDestroy, onMount } from "svelte";
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule"; import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control"; import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
import type { Settings } from "$lib/models/settings";
export let trails: Trail[] = []; export let trails: Trail[] = [];
export let markers: M.Marker[] = []; export let markers: M.Marker[] = [];
@@ -27,6 +28,7 @@
export let showGrid: boolean = false; export let showGrid: boolean = false;
export let showStyleSwitcher: boolean = true; export let showStyleSwitcher: boolean = true;
export let showFullscreen: boolean = false; export let showFullscreen: boolean = false;
export let showTerrain: boolean = false;
export let fitBounds: "animate" | "instant" | "off" = "instant"; export let fitBounds: "animate" | "instant" | "off" = "instant";
export let elevationProfileContainer: string | HTMLDivElement | undefined = export let elevationProfileContainer: string | HTMLDivElement | undefined =
@@ -194,6 +196,11 @@
const bounds = data[activeTrail] const bounds = data[activeTrail]
? (data[activeTrail].bbox as M.LngLatBoundsLike) ? (data[activeTrail].bbox as M.LngLatBoundsLike)
: getBounds(); : getBounds();
if (!bounds) {
return;
}
map!.fitBounds(bounds, { map!.fitBounds(bounds, {
animate: animate, animate: animate,
padding: { padding: {
@@ -336,8 +343,6 @@
dispatch("select", trail); dispatch("select", trail);
const index = trails.findIndex((t) => t.id == trail.id); const index = trails.findIndex((t) => t.id == trail.id);
if (index == -1) { if (index == -1) {
console.log("here");
return; return;
} }
activeTrail = index; activeTrail = index;
@@ -425,30 +430,37 @@
) )
).ElevationProfileControl; ).ElevationProfileControl;
const mapStyles = [ const mapStyles: { text: string; value: string; thumbnail?: string }[] =
{ [
text: "Open Street Maps", ...(($page.data.settings as Settings).tilesets ?? []).map(
value: "/styles/osm.json", (t) => ({
thumbnail: "https://tile.openstreetmap.org/1/0/0.png", text: t.name,
}, value: t.url,
{ }),
text: "Open Topo Maps", ),
value: "/styles/otm.json", {
thumbnail: "https://tile.opentopomap.org/1/0/0.png", text: "Open Street Maps",
}, value: "/styles/osm.json",
{ thumbnail: "https://tile.openstreetmap.org/1/0/0.png",
text: "Carto Light", },
value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", {
thumbnail: text: "Open Topo Maps",
"https://basemaps.cartocdn.com/light_all/1/0/0@2x.png", value: "/styles/otm.json",
}, thumbnail: "https://tile.opentopomap.org/1/0/0.png",
{ },
text: "Carto Dark", {
value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json", text: "Carto Light",
thumbnail: value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
"https://basemaps.cartocdn.com/dark_all/1/0/0@2x.png", thumbnail:
}, "https://basemaps.cartocdn.com/light_all/1/0/0@2x.png",
]; },
{
text: "Carto Dark",
value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
thumbnail:
"https://basemaps.cartocdn.com/dark_all/1/0/0@2x.png",
},
];
const preferredMapStyleIndex = mapStyles.findIndex( const preferredMapStyleIndex = mapStyles.findIndex(
(s) => s.text === localStorage.getItem("layer"), (s) => s.text === localStorage.getItem("layer"),
); );
@@ -490,7 +502,9 @@
selectedIndex: selectedIndex:
preferredMapStyleIndex !== -1 ? preferredMapStyleIndex : 0, preferredMapStyleIndex !== -1 ? preferredMapStyleIndex : 0,
}); });
map.addControl(new M.NavigationControl()); map.addControl(
new M.NavigationControl({ visualizePitch: showTerrain }),
);
map.addControl( map.addControl(
new M.ScaleControl({ new M.ScaleControl({
maxWidth: 120, maxWidth: 120,
@@ -536,10 +550,25 @@
); );
} }
if (showTerrain) {
map!.addControl(
new M.TerrainControl({
source: "terrain",
}),
);
}
map.on("styledata", () => { map.on("styledata", () => {
trails.forEach((t, i) => { trails.forEach((t, i) => {
addTrailLayer(t, t.id ?? i.toString(), data?.at(i)); addTrailLayer(t, t.id ?? i.toString(), data?.at(i));
}); });
if (showTerrain && $page.data.settings?.terrain) {
map!.addSource("terrain", {
type: "raster-dem",
url: $page.data.settings.terrain,
});
}
}); });
map.on("moveend", (e) => { map.on("moveend", (e) => {

View File

@@ -7,6 +7,7 @@ class Settings {
location?: { name: string, lat: number, lon: number }; location?: { name: string, lat: number, lon: number };
category?: string; category?: string;
tilesets?: {name: string, url: string}[] tilesets?: {name: string, url: string}[]
terrain?: string;
user?: string; user?: string;
constructor( constructor(
@@ -18,6 +19,7 @@ class Settings {
location?: { name: string, lat: number, lon: number } location?: { name: string, lat: number, lon: number }
category?: string category?: string
tilesets?: {name: string, url: string}[] tilesets?: {name: string, url: string}[]
terrain?: string;
} }
) { ) {
this.unit = unit; this.unit = unit;
@@ -27,6 +29,7 @@ class Settings {
this.location = params?.location; this.location = params?.location;
this.category = params?.category; this.category = params?.category;
this.tilesets = params?.tilesets ?? []; this.tilesets = params?.tilesets ?? [];
this.terrain = params?.terrain;
} }
} }

View File

@@ -85,7 +85,7 @@ export function createPopupFromTrail(trail: Trail) {
: "/imgs/default_thumbnail.webp"; : "/imgs/default_thumbnail.webp";
const popup = new M.Popup({maxWidth: "320px"}); const popup = new M.Popup({maxWidth: "320px"});
popup.setHTML( popup.setHTML(
`<a href="/trail/view/${trail.id}" data-sveltekit-preload-data="off"> `<a href="/map/trail/${trail.id}" data-sveltekit-preload-data="off">
<li class="flex items-center gap-4 cursor-pointer text-black max-w-80"> <li class="flex items-center gap-4 cursor-pointer text-black max-w-80">
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt=""> <div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
</div> </div>

View File

@@ -415,7 +415,6 @@ export class ElevationProfile {
Chart.register(...registerables); Chart.register(...registerables);
Chart.register(zoomPlugin); Chart.register(zoomPlugin);
Chart.register(CrosshairPlugin);
this.settings = { this.settings = {
...elevationProfileDefaultOptions, ...elevationProfileDefaultOptions,
@@ -676,6 +675,7 @@ export class ElevationProfile {
}, },
plugins: [ plugins: [
CrosshairPlugin,
{ {
id: "waypointPlugin", id: "waypointPlugin",
afterDraw: (chart, args, options) => { afterDraw: (chart, args, options) => {

View File

@@ -7,7 +7,7 @@ import { type ControlPosition, type IControl } from "maplibre-gl";
type MapStyle = { type MapStyle = {
text: string; text: string;
value: string; value: string;
thumbnail: string; thumbnail?: string;
} }
export type StyleSwitcherControlOptions = { export type StyleSwitcherControlOptions = {
@@ -73,20 +73,30 @@ export class StyleSwitcherControl implements IControl {
this.switcherContainer.appendChild(headingDiv) this.switcherContainer.appendChild(headingDiv)
const buttonDiv = document.createElement("ul") const buttonDiv = document.createElement("ul")
buttonDiv.classList.add("mt-2"); buttonDiv.classList.add("mt-2", "max-h-64", "overflow-y-scroll");
this.settings.styles.forEach((style, i) => { this.settings.styles.forEach((style, i) => {
const styleLi = document.createElement("li"); const styleLi = document.createElement("li");
styleLi.classList.add("flex", "items-center", "gap-x-4", "px-3", "py-2", "cursor-pointer", "hover:bg-menu-item-background-hover") styleLi.classList.add("flex", "items-center", "gap-x-4", "px-3", "py-2", "cursor-pointer", "hover:bg-menu-item-background-hover")
const styleImg = document.createElement("img"); if (style.thumbnail) {
styleImg.classList.add("w-12", "h-12", "rounded-md") const styleImg = document.createElement("img");
styleImg.src = style.thumbnail; styleImg.classList.add("w-12", "h-12", "rounded-md")
styleImg.src = style.thumbnail;
styleLi.appendChild(styleImg)
} else {
const styleIconContainer = document.createElement("i");
styleIconContainer.classList.add("w-12", "h-12", "rounded-md", "bg-blue-200", "flex", "items-center", "justify-center")
const styleIcon = document.createElement("i");
styleIcon.classList.add("fa", "fa-map-location-dot", "text-xl")
styleIconContainer.appendChild(styleIcon)
styleLi.appendChild(styleIconContainer)
}
const styleName = document.createElement("span"); const styleName = document.createElement("span");
styleName.classList.add("!text-sm") styleName.classList.add("!text-sm")
if (i == this.settings.selectedIndex) { if (i == this.settings.selectedIndex) {
styleName.classList.add("font-semibold") styleName.classList.add("font-semibold")
} }
styleName.textContent = style.text; styleName.textContent = style.text;
styleLi.appendChild(styleImg)
styleLi.appendChild(styleName) styleLi.appendChild(styleName)
styleLi.addEventListener("click", () => { styleLi.addEventListener("click", () => {

View File

@@ -2,6 +2,7 @@
import '$lib/i18n'; import '$lib/i18n';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
import { env } from "$env/dynamic/private"; import { env } from "$env/dynamic/private";
import type { Settings } from '$lib/models/settings';
export const load: LayoutServerLoad = async ({ locals, url }) => { export const load: LayoutServerLoad = async ({ locals, url }) => {
return { settings: locals.settings, origin: env.ORIGIN } return { settings: locals.settings, origin: env.ORIGIN }

View File

@@ -27,7 +27,7 @@
let map: M.Map; let map: M.Map;
let mapWithElevation: MapWithElevationMaplibre; let mapWithElevation: MapWithElevationMaplibre;
let markers: any[]; let markers: M.Marker[];
let showMap: boolean = true; let showMap: boolean = true;
let selectedList: List | null = null; let selectedList: List | null = null;

View File

@@ -15,7 +15,7 @@
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4"> <main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel> <TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
<div id="trail-details" class="sticky top-[62px]"> <div id="trail-details" class="sticky top-[62px]">
<MapWithElevationMaplibre trails={[$trail]} bind:markers></MapWithElevationMaplibre> <MapWithElevationMaplibre trails={[$trail]} bind:markers showTerrain={true}></MapWithElevationMaplibre>
</div> </div>
</main> </main>

View File

@@ -49,11 +49,14 @@
let customTilesetName: string = ""; let customTilesetName: string = "";
let customTilesetURL: string = ""; let customTilesetURL: string = "";
let terrainURL: string = "";
onMount(() => { onMount(() => {
citySearchQuery = settings?.location?.name ?? ""; citySearchQuery = settings?.location?.name ?? "";
selectedLanguage = settings?.language || "en"; selectedLanguage = settings?.language || "en";
selectedMapFocus = settings?.mapFocus ?? "trails"; selectedMapFocus = settings?.mapFocus ?? "trails";
terrainURL = settings?.terrain ?? "";
}); });
async function searchCities(q: string) { async function searchCities(q: string) {
@@ -127,6 +130,13 @@
tilesets: settings.tilesets, tilesets: settings.tilesets,
}); });
} }
async function handleTerrainAdd() {
await settings_update({
id: settings!.id,
terrain: terrainURL,
});
}
</script> </script>
<svelte:head> <svelte:head>
@@ -189,14 +199,14 @@
<TextField <TextField
label={$_("name")} label={$_("name")}
bind:value={customTilesetName} bind:value={customTilesetName}
placeholder="Open Street Maps" placeholder={$_("name")}
></TextField> ></TextField>
<div class="flex items-center basis-full gap-2"> <div class="flex items-center basis-full gap-2">
<div class="flex-grow"> <div class="flex-grow">
<TextField <TextField
label="URL" label="URL"
bind:value={customTilesetURL} bind:value={customTilesetURL}
placeholder="https://{'{'}s{'}'}.tile.openstreetmap.org/{'{'}z{'}'}/{'{'}x{'}'}/{'{'}y{'}'}.png" placeholder="https://.../style.json"
></TextField> ></TextField>
</div> </div>
<button <button
@@ -206,6 +216,24 @@
> >
</div> </div>
</div> </div>
<h3 class="text-2xl font-semibold">{$_("Terrain")}</h3>
<div class="flex items-center gap-2">
<div class="basis-full">
<TextField
label="Terrain URL"
bind:value={terrainURL}
placeholder="https://.../tiles.json"
></TextField>
</div>
<button
disabled={terrainURL == settings?.terrain}
class="btn-icon mt-6"
class:hover:!bg-background={terrainURL == settings?.terrain}
on:click={handleTerrainAdd}
class:text-gray-500={terrainURL == settings?.terrain}
><i class="fa fa-save"></i></button
>
</div>
</div> </div>
</Settings> </Settings>
{/if} {/if}