mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-02-06 16:33:09 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c6e03f4a8 | ||
|
|
2a4dfe010e | ||
|
|
f42a916c25 | ||
|
|
772b810fa8 | ||
|
|
4d4d10d5c2 | ||
|
|
0e4c7dbe64 |
@@ -34,11 +34,10 @@
|
|||||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
import { selection, copied, cut } from '$lib/logic/selection';
|
import { selection, copied, cut } from '$lib/logic/selection';
|
||||||
import { map } from '$lib/components/map/map';
|
|
||||||
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
|
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
import { boundsManager } from '$lib/logic/bounds';
|
import { boundsManager } from '$lib/logic/bounds';
|
||||||
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
||||||
import { allowedPastes } from './sortable-file-list';
|
import { allowedPastes } from './sortable-file-list';
|
||||||
@@ -58,19 +57,11 @@
|
|||||||
|
|
||||||
let singleSelection = $derived($selection.size === 1);
|
let singleSelection = $derived($selection.size === 1);
|
||||||
|
|
||||||
let nodeColors: string[] = $state([]);
|
let nodeColors: string[] = $derived.by(() => {
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
let colors: string[] = [];
|
let colors: string[] = [];
|
||||||
if (node && $map) {
|
if (node) {
|
||||||
if (node instanceof GPXFile) {
|
if (node instanceof GPXFile) {
|
||||||
let defaultColor = undefined;
|
let defaultColor = $gpxColors.get(item.getFileId());
|
||||||
|
|
||||||
let layer = gpxLayers.getLayer(item.getFileId());
|
|
||||||
if (layer) {
|
|
||||||
defaultColor = layer.layerColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = node.getStyle(defaultColor);
|
let style = node.getStyle(defaultColor);
|
||||||
colors = style.color;
|
colors = style.color;
|
||||||
} else if (node instanceof Track) {
|
} else if (node instanceof Track) {
|
||||||
@@ -83,14 +74,14 @@
|
|||||||
colors.push(style['gpx_style:color']);
|
colors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
if (colors.length === 0) {
|
if (colors.length === 0) {
|
||||||
let layer = gpxLayers.getLayer(item.getFileId());
|
let defaultColor = $gpxColors.get(item.getFileId());
|
||||||
if (layer) {
|
if (defaultColor) {
|
||||||
colors.push(layer.layerColor);
|
colors.push(defaultColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodeColors = colors;
|
return colors;
|
||||||
});
|
});
|
||||||
|
|
||||||
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
language = 'en';
|
language = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
|
map.init(language, hash, geocoder, geolocate);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
|
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
|
||||||
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
|
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
|
||||||
@@ -9,13 +9,10 @@
|
|||||||
let distanceMarkers: DistanceMarkers;
|
let distanceMarkers: DistanceMarkers;
|
||||||
let startEndMarkers: StartEndMarkers;
|
let startEndMarkers: StartEndMarkers;
|
||||||
|
|
||||||
onMount(() => {
|
map.onLoad((map_) => {
|
||||||
gpxLayers.init();
|
gpxLayers.init();
|
||||||
startEndMarkers = new StartEndMarkers();
|
startEndMarkers = new StartEndMarkers();
|
||||||
distanceMarkers = new DistanceMarkers();
|
distanceMarkers = new DistanceMarkers();
|
||||||
});
|
|
||||||
|
|
||||||
map.onLoad((map_) => {
|
|
||||||
createPopups(map_);
|
createPopups(map_);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
class="justify-start"
|
||||||
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { gpxStatistics } from '$lib/logic/statistics';
|
|||||||
import { getConvertedDistanceToKilometers } from '$lib/units';
|
import { getConvertedDistanceToKilometers } from '$lib/units';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
@@ -44,44 +44,45 @@ export class DistanceMarkers {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!map_.getLayer('distance-markers')) {
|
if (!map_.getLayer('distance-markers')) {
|
||||||
map_.addLayer({
|
map_.addLayer(
|
||||||
id: 'distance-markers',
|
{
|
||||||
type: 'symbol',
|
id: 'distance-markers',
|
||||||
source: 'distance-markers',
|
type: 'symbol',
|
||||||
filter: [
|
source: 'distance-markers',
|
||||||
'match',
|
filter: [
|
||||||
['get', 'level'],
|
'match',
|
||||||
100,
|
['get', 'level'],
|
||||||
['>=', ['zoom'], 0],
|
100,
|
||||||
50,
|
['>=', ['zoom'], 0],
|
||||||
['>=', ['zoom'], 7],
|
50,
|
||||||
25,
|
['>=', ['zoom'], 7],
|
||||||
[
|
25,
|
||||||
'any',
|
[
|
||||||
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
'any',
|
||||||
|
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
||||||
|
['>=', ['zoom'], 11],
|
||||||
|
],
|
||||||
|
10,
|
||||||
|
['>=', ['zoom'], 10],
|
||||||
|
5,
|
||||||
['>=', ['zoom'], 11],
|
['>=', ['zoom'], 11],
|
||||||
|
1,
|
||||||
|
['>=', ['zoom'], 13],
|
||||||
|
false,
|
||||||
],
|
],
|
||||||
10,
|
layout: {
|
||||||
['>=', ['zoom'], 10],
|
'text-field': ['get', 'distance'],
|
||||||
5,
|
'text-size': 14,
|
||||||
['>=', ['zoom'], 11],
|
'text-font': ['Open Sans Bold'],
|
||||||
1,
|
},
|
||||||
['>=', ['zoom'], 13],
|
paint: {
|
||||||
false,
|
'text-color': 'black',
|
||||||
],
|
'text-halo-width': 2,
|
||||||
layout: {
|
'text-halo-color': 'white',
|
||||||
'text-field': ['get', 'distance'],
|
},
|
||||||
'text-size': 14,
|
|
||||||
'text-font': ['Open Sans Bold'],
|
|
||||||
},
|
},
|
||||||
paint: {
|
ANCHOR_LAYER_KEY.distanceMarkers
|
||||||
'text-color': 'black',
|
);
|
||||||
'text-halo-width': 2,
|
|
||||||
'text-halo-color': 'white',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
map_.moveLayer('distance-markers');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (map_.getLayer('distance-markers')) {
|
if (map_.getLayer('distance-markers')) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
||||||
import {
|
import {
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
@@ -22,6 +22,7 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
|
|||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
|
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000',
|
'#ff0000',
|
||||||
@@ -43,16 +44,35 @@ for (let color of colors) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the color with the least amount of uses
|
// Get the color with the least amount of uses
|
||||||
function getColor() {
|
function getColor(fileId: string) {
|
||||||
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||||
colorCount[color]++;
|
colorCount[color]++;
|
||||||
|
gpxColors.update((colors) => {
|
||||||
|
colors.set(fileId, color);
|
||||||
|
return colors;
|
||||||
|
});
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrementColor(color: string) {
|
function replaceColor(fileId: string, oldColor: string, newColor: string) {
|
||||||
|
if (colorCount.hasOwnProperty(oldColor)) {
|
||||||
|
colorCount[oldColor]--;
|
||||||
|
}
|
||||||
|
colorCount[newColor]++;
|
||||||
|
gpxColors.update((colors) => {
|
||||||
|
colors.set(fileId, newColor);
|
||||||
|
return colors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeColor(fileId: string, color: string) {
|
||||||
if (colorCount.hasOwnProperty(color)) {
|
if (colorCount.hasOwnProperty(color)) {
|
||||||
colorCount[color]--;
|
colorCount[color]--;
|
||||||
}
|
}
|
||||||
|
gpxColors.update((colors) => {
|
||||||
|
colors.delete(fileId);
|
||||||
|
return colors;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
||||||
@@ -121,7 +141,7 @@ export class GPXLayer {
|
|||||||
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.layerColor = getColor();
|
this.layerColor = getColor(fileId);
|
||||||
this.unsubscribe.push(
|
this.unsubscribe.push(
|
||||||
map.subscribe(($map) => {
|
map.subscribe(($map) => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
@@ -158,7 +178,7 @@ export class GPXLayer {
|
|||||||
file._data.style.color &&
|
file._data.style.color &&
|
||||||
this.layerColor !== `#${file._data.style.color}`
|
this.layerColor !== `#${file._data.style.color}`
|
||||||
) {
|
) {
|
||||||
decrementColor(this.layerColor);
|
replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`);
|
||||||
this.layerColor = `#${file._data.style.color}`;
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,20 +196,23 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_map.getLayer(this.fileId)) {
|
if (!_map.getLayer(this.fileId)) {
|
||||||
_map.addLayer({
|
_map.addLayer(
|
||||||
id: this.fileId,
|
{
|
||||||
type: 'line',
|
id: this.fileId,
|
||||||
source: this.fileId,
|
type: 'line',
|
||||||
layout: {
|
source: this.fileId,
|
||||||
'line-join': 'round',
|
layout: {
|
||||||
'line-cap': 'round',
|
'line-join': 'round',
|
||||||
|
'line-cap': 'round',
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'color'],
|
||||||
|
'line-width': ['get', 'width'],
|
||||||
|
'line-opacity': ['get', 'opacity'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
paint: {
|
ANCHOR_LAYER_KEY.tracks
|
||||||
'line-color': ['get', 'color'],
|
);
|
||||||
'line-width': ['get', 'width'],
|
|
||||||
'line-opacity': ['get', 'opacity'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
_map.on('click', this.fileId, this.layerOnClickBinded);
|
_map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
@@ -212,18 +235,21 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
||||||
_map.addLayer({
|
_map.addLayer(
|
||||||
id: this.fileId + '-waypoints',
|
{
|
||||||
type: 'symbol',
|
id: this.fileId + '-waypoints',
|
||||||
source: this.fileId + '-waypoints',
|
type: 'symbol',
|
||||||
layout: {
|
source: this.fileId + '-waypoints',
|
||||||
'icon-image': ['get', 'icon'],
|
layout: {
|
||||||
'icon-size': 0.3,
|
'icon-image': ['get', 'icon'],
|
||||||
'icon-anchor': 'bottom',
|
'icon-size': 0.3,
|
||||||
'icon-padding': 0,
|
'icon-anchor': 'bottom',
|
||||||
'icon-allow-overlap': true,
|
'icon-padding': 0,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
ANCHOR_LAYER_KEY.waypoints
|
||||||
|
);
|
||||||
|
|
||||||
_map.on(
|
_map.on(
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
@@ -272,7 +298,7 @@ export class GPXLayer {
|
|||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
_map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
ANCHOR_LAYER_KEY.directionMarkers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -364,7 +390,7 @@ export class GPXLayer {
|
|||||||
|
|
||||||
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
decrementColor(this.layerColor);
|
removeColor(this.fileId, this.layerColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToFront() {
|
moveToFront() {
|
||||||
@@ -373,13 +399,13 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId)) {
|
if (_map.getLayer(this.fileId)) {
|
||||||
_map.moveLayer(this.fileId);
|
_map.moveLayer(this.fileId, ANCHOR_LAYER_KEY.tracks);
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
if (_map.getLayer(this.fileId + '-waypoints')) {
|
||||||
_map.moveLayer(this.fileId + '-waypoints');
|
_map.moveLayer(this.fileId + '-waypoints', ANCHOR_LAYER_KEY.waypoints);
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.moveLayer(this.fileId + '-direction');
|
_map.moveLayer(this.fileId + '-direction', ANCHOR_LAYER_KEY.directionMarkers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
import { GPXLayer } from './gpx-layer';
|
import { GPXLayer } from './gpx-layer';
|
||||||
|
|
||||||
export class GPXLayerCollection {
|
export class GPXLayerCollection {
|
||||||
@@ -42,3 +43,4 @@ export class GPXLayerCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const gpxLayers = new GPXLayerCollection();
|
export const gpxLayers = new GPXLayerCollection();
|
||||||
|
export const gpxColors = writable(new Map<string, string>());
|
||||||
|
|||||||
@@ -54,28 +54,27 @@
|
|||||||
|
|
||||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
||||||
<Card.Header class="p-0 gap-0">
|
<Card.Header class="p-0 gap-0">
|
||||||
<Card.Title class="text-md">
|
<Card.Title class="text-md flex flex-row">
|
||||||
<div class="flex flex-row gap-3">
|
<div class="flex flex-col">
|
||||||
<div class="flex flex-col">
|
<p>{name}</p>
|
||||||
{name}
|
<div class="text-muted-foreground text-xs font-normal">
|
||||||
<div class="text-muted-foreground text-xs font-normal">
|
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
||||||
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
class="ml-auto"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
|
|
||||||
'node'}={poi.item.id}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<PencilLine size="16" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="ml-auto"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? 'node'}={poi
|
||||||
|
.item.id}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<PencilLine size="16" />
|
||||||
|
</Button>
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
|
<Card.Content class="flex flex-col gap-1 p-0 text-sm whitespace-normal break-all">
|
||||||
<ScrollArea class="flex flex-col max-h-[30dvh]">
|
<ScrollArea class="flex flex-col max-h-[30dvh]">
|
||||||
{#if tags.image || tags['image:0']}
|
{#if tags.image || tags['image:0']}
|
||||||
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
||||||
@@ -100,8 +99,14 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
|
<Button
|
||||||
<MapPin size="16" />
|
size="sm"
|
||||||
|
class="mt-1 justify-start"
|
||||||
|
variant="outline"
|
||||||
|
disabled={$selection.size === 0}
|
||||||
|
onclick={addToFile}
|
||||||
|
>
|
||||||
|
<MapPin size="14" />
|
||||||
{i18n._('toolbar.waypoint.add')}
|
{i18n._('toolbar.waypoint.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { overpassQueryData } from '$lib/assets/layers';
|
|||||||
import { MapPopup } from '$lib/components/map/map-popup';
|
import { MapPopup } from '$lib/components/map/map-popup';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
|
|
||||||
const { currentOverpassQueries } = settings;
|
const { currentOverpassQueries } = settings;
|
||||||
|
|
||||||
@@ -85,17 +86,20 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.map.getLayer('overpass')) {
|
if (!this.map.getLayer('overpass')) {
|
||||||
this.map.addLayer({
|
this.map.addLayer(
|
||||||
id: 'overpass',
|
{
|
||||||
type: 'symbol',
|
id: 'overpass',
|
||||||
source: 'overpass',
|
type: 'symbol',
|
||||||
layout: {
|
source: 'overpass',
|
||||||
'icon-image': ['get', 'icon'],
|
layout: {
|
||||||
'icon-size': 0.25,
|
'icon-image': ['get', 'icon'],
|
||||||
'icon-padding': 0,
|
'icon-size': 0.25,
|
||||||
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
'icon-padding': 0,
|
||||||
|
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
ANCHOR_LAYER_KEY.overpass
|
||||||
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||||
|
|||||||
@@ -20,6 +20,28 @@ let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
|||||||
easing: () => 1,
|
easing: () => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emptySource: mapboxgl.GeoJSONSourceSpecification = {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const ANCHOR_LAYER_KEY = {
|
||||||
|
mapillary: 'mapillary-end',
|
||||||
|
tracks: 'tracks-end',
|
||||||
|
directionMarkers: 'direction-markers-end',
|
||||||
|
distanceMarkers: 'distance-markers-end',
|
||||||
|
interactions: 'interactions-end',
|
||||||
|
overpass: 'overpass-end',
|
||||||
|
waypoints: 'waypoints-end',
|
||||||
|
};
|
||||||
|
const anchorLayers: mapboxgl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
|
||||||
|
id: id,
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'empty-source',
|
||||||
|
}));
|
||||||
|
|
||||||
export class MapboxGLMap {
|
export class MapboxGLMap {
|
||||||
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
||||||
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
||||||
@@ -29,19 +51,15 @@ export class MapboxGLMap {
|
|||||||
return this._map.subscribe(run, invalidate);
|
return this._map.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(language: string, hash: boolean, geocoder: boolean, geolocate: boolean) {
|
||||||
accessToken: string,
|
|
||||||
language: string,
|
|
||||||
hash: boolean,
|
|
||||||
geocoder: boolean,
|
|
||||||
geolocate: boolean
|
|
||||||
) {
|
|
||||||
const map = new mapboxgl.Map({
|
const map = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: {
|
style: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {},
|
sources: {
|
||||||
layers: [],
|
'empty-source': emptySource,
|
||||||
|
},
|
||||||
|
layers: anchorLayers,
|
||||||
imports: [
|
imports: [
|
||||||
{
|
{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
@@ -50,11 +68,6 @@ export class MapboxGLMap {
|
|||||||
{
|
{
|
||||||
id: 'overlays',
|
id: 'overlays',
|
||||||
url: '',
|
url: '',
|
||||||
data: {
|
|
||||||
version: 8,
|
|
||||||
sources: {},
|
|
||||||
layers: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } fro
|
|||||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
@@ -99,10 +100,10 @@ export class MapillaryLayer {
|
|||||||
this.map.addSource('mapillary', mapillarySource);
|
this.map.addSource('mapillary', mapillarySource);
|
||||||
}
|
}
|
||||||
if (!this.map.getLayer('mapillary-sequence')) {
|
if (!this.map.getLayer('mapillary-sequence')) {
|
||||||
this.map.addLayer(mapillarySequenceLayer);
|
this.map.addLayer(mapillarySequenceLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||||
}
|
}
|
||||||
if (!this.map.getLayer('mapillary-image')) {
|
if (!this.map.getLayer('mapillary-image')) {
|
||||||
this.map.addLayer(mapillaryImageLayer);
|
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||||
}
|
}
|
||||||
this.map.on('style.load', this.addBinded);
|
this.map.on('style.load', this.addBinded);
|
||||||
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { Trash2 } from '@lucide/svelte';
|
import { Trash2 } from '@lucide/svelte';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
@@ -63,15 +63,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('rectangle')) {
|
if (!$map.getLayer('rectangle')) {
|
||||||
$map.addLayer({
|
$map.addLayer(
|
||||||
id: 'rectangle',
|
{
|
||||||
type: 'fill',
|
id: 'rectangle',
|
||||||
source: 'rectangle',
|
type: 'fill',
|
||||||
paint: {
|
source: 'rectangle',
|
||||||
'fill-color': 'SteelBlue',
|
paint: {
|
||||||
'fill-opacity': 0.5,
|
'fill-color': 'SteelBlue',
|
||||||
|
'fill-opacity': 0.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
ANCHOR_LAYER_KEY.interactions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { MountainSnow } from '@lucide/svelte';
|
import { MountainSnow } from '@lucide/svelte';
|
||||||
import { map } from '$lib/components/map/map';
|
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
@@ -20,11 +19,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={() => {
|
onclick={() => fileActions.addElevationToSelection()}
|
||||||
if ($map) {
|
|
||||||
fileActions.addElevationToSelection($map);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MountainSnow size="16" class="shrink-0" />
|
<MountainSnow size="16" class="shrink-0" />
|
||||||
{i18n._('toolbar.elevation.button')}
|
{i18n._('toolbar.elevation.button')}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
@@ -144,17 +144,18 @@ export class ReducedGPXLayerCollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!map_.getLayer('simplified')) {
|
if (!map_.getLayer('simplified')) {
|
||||||
map_.addLayer({
|
map_.addLayer(
|
||||||
id: 'simplified',
|
{
|
||||||
type: 'line',
|
id: 'simplified',
|
||||||
source: 'simplified',
|
type: 'line',
|
||||||
paint: {
|
source: 'simplified',
|
||||||
'line-color': 'white',
|
paint: {
|
||||||
'line-width': 3,
|
'line-color': 'white',
|
||||||
|
'line-width': 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
ANCHOR_LAYER_KEY.interactions
|
||||||
} else {
|
);
|
||||||
map_.moveLayer('simplified');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { get } from 'svelte/store';
|
|||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -108,24 +109,25 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.map.getLayer('split-controls')) {
|
if (!this.map.getLayer('split-controls')) {
|
||||||
this.map.addLayer({
|
this.map.addLayer(
|
||||||
id: 'split-controls',
|
{
|
||||||
type: 'symbol',
|
id: 'split-controls',
|
||||||
source: 'split-controls',
|
type: 'symbol',
|
||||||
layout: {
|
source: 'split-controls',
|
||||||
'icon-image': 'split-control',
|
layout: {
|
||||||
'icon-size': 0.25,
|
'icon-image': 'split-control',
|
||||||
'icon-padding': 0,
|
'icon-size': 0.25,
|
||||||
|
'icon-padding': 0,
|
||||||
|
},
|
||||||
|
filter: ['<=', ['get', 'minZoom'], ['zoom']],
|
||||||
},
|
},
|
||||||
filter: ['<=', ['get', 'minZoom'], ['zoom']],
|
ANCHOR_LAYER_KEY.interactions
|
||||||
});
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.map.moveLayer('split-controls');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -807,7 +807,7 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addElevationToSelection: async (map: mapboxgl.Map) => {
|
addElevationToSelection: async () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
"update": "更新图层"
|
"update": "更新图层"
|
||||||
},
|
},
|
||||||
"opacity": "图层透明度",
|
"opacity": "图层透明度",
|
||||||
"terrain": "Terrain source",
|
"terrain": "地形来源",
|
||||||
"label": {
|
"label": {
|
||||||
"basemaps": "底图",
|
"basemaps": "底图",
|
||||||
"overlays": "叠加层",
|
"overlays": "叠加层",
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"mapterhornHillshade": "Mapterhorn Hillshade",
|
"mapterhornHillshade": "山体阴影",
|
||||||
"openRailwayMap": "OpenRailwayMap",
|
"openRailwayMap": "OpenRailwayMap",
|
||||||
"swisstopoSlope": "Swisstopo Slope",
|
"swisstopoSlope": "Swisstopo Slope",
|
||||||
"swisstopoHiking": "Swisstopo Hiking",
|
"swisstopoHiking": "Swisstopo Hiking",
|
||||||
|
|||||||
Reference in New Issue
Block a user