adds terrain to map
This commit is contained in:
54
db/migrations/1733093484_updated_settings.go
Normal file
54
db/migrations/1733093484_updated_settings.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user