mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-02-06 00:13:09 +00:00
use map layers for start/end/hover markers
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
Construction,
|
Construction,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||||
@@ -28,12 +28,14 @@
|
|||||||
let {
|
let {
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
|
hoveredPoint,
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
showControls = true,
|
showControls = true,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
|
hoveredPoint: Writable<Coordinates | null>;
|
||||||
additionalDatasets: Writable<string[]>;
|
additionalDatasets: Writable<string[]>;
|
||||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
showControls?: boolean;
|
showControls?: boolean;
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
elevationProfile = new ElevationProfile(
|
elevationProfile = new ElevationProfile(
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
|
hoveredPoint,
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
canvas,
|
canvas,
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import Chart, {
|
|||||||
type ScriptableLineSegmentContext,
|
type ScriptableLineSegmentContext,
|
||||||
type TooltipItem,
|
type TooltipItem,
|
||||||
} from 'chart.js/auto';
|
} from 'chart.js/auto';
|
||||||
import maplibregl from 'maplibre-gl';
|
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ interface ElevationProfilePoint {
|
|||||||
length: number;
|
length: number;
|
||||||
};
|
};
|
||||||
extensions: Record<string, any>;
|
extensions: Record<string, any>;
|
||||||
coordinates: [number, number];
|
coordinates: Coordinates;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,18 +48,19 @@ export class ElevationProfile {
|
|||||||
private _chart: Chart | null = null;
|
private _chart: Chart | null = null;
|
||||||
private _canvas: HTMLCanvasElement;
|
private _canvas: HTMLCanvasElement;
|
||||||
private _overlay: HTMLCanvasElement;
|
private _overlay: HTMLCanvasElement;
|
||||||
private _marker: maplibregl.Marker | null = null;
|
|
||||||
private _dragging = false;
|
private _dragging = false;
|
||||||
private _panning = false;
|
private _panning = false;
|
||||||
|
|
||||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
|
private _hoveredPoint: Writable<Coordinates | null>;
|
||||||
private _additionalDatasets: Readable<string[]>;
|
private _additionalDatasets: Readable<string[]>;
|
||||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||||
|
hoveredPoint: Writable<Coordinates | null>,
|
||||||
additionalDatasets: Readable<string[]>,
|
additionalDatasets: Readable<string[]>,
|
||||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@@ -69,17 +68,12 @@ export class ElevationProfile {
|
|||||||
) {
|
) {
|
||||||
this._gpxStatistics = gpxStatistics;
|
this._gpxStatistics = gpxStatistics;
|
||||||
this._slicedGPXStatistics = slicedGPXStatistics;
|
this._slicedGPXStatistics = slicedGPXStatistics;
|
||||||
|
this._hoveredPoint = hoveredPoint;
|
||||||
this._additionalDatasets = additionalDatasets;
|
this._additionalDatasets = additionalDatasets;
|
||||||
this._elevationFill = elevationFill;
|
this._elevationFill = elevationFill;
|
||||||
this._canvas = canvas;
|
this._canvas = canvas;
|
||||||
this._overlay = overlay;
|
this._overlay = overlay;
|
||||||
|
|
||||||
let element = document.createElement('div');
|
|
||||||
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
|
||||||
this._marker = new maplibregl.Marker({
|
|
||||||
element,
|
|
||||||
});
|
|
||||||
|
|
||||||
import('chartjs-plugin-zoom').then((module) => {
|
import('chartjs-plugin-zoom').then((module) => {
|
||||||
Chart.register(module.default);
|
Chart.register(module.default);
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@@ -162,14 +156,10 @@ export class ElevationProfile {
|
|||||||
label: (context: TooltipItem<'line'>) => {
|
label: (context: TooltipItem<'line'>) => {
|
||||||
let point = context.raw as ElevationProfilePoint;
|
let point = context.raw as ElevationProfilePoint;
|
||||||
if (context.datasetIndex === 0) {
|
if (context.datasetIndex === 0) {
|
||||||
const map_ = get(map);
|
|
||||||
if (map_ && this._marker) {
|
|
||||||
if (this._dragging) {
|
if (this._dragging) {
|
||||||
this._marker.remove();
|
this._hoveredPoint.set(null);
|
||||||
} else {
|
} else {
|
||||||
this._marker.setLngLat(point.coordinates);
|
this._hoveredPoint.set(point.coordinates);
|
||||||
this._marker.addTo(map_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
||||||
} else if (context.datasetIndex === 1) {
|
} else if (context.datasetIndex === 1) {
|
||||||
@@ -312,10 +302,7 @@ export class ElevationProfile {
|
|||||||
events: ['mouseout'],
|
events: ['mouseout'],
|
||||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||||
if (args.event.type === 'mouseout') {
|
if (args.event.type === 'mouseout') {
|
||||||
const map_ = get(map);
|
this._hoveredPoint.set(null);
|
||||||
if (map_ && this._marker) {
|
|
||||||
this._marker.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -637,8 +624,5 @@ export class ElevationProfile {
|
|||||||
this._chart.destroy();
|
this._chart.destroy();
|
||||||
this._chart = null;
|
this._chart = null;
|
||||||
}
|
}
|
||||||
if (this._marker) {
|
|
||||||
this._marker.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { setMode } from 'mode-watcher';
|
import { setMode } from 'mode-watcher';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import { loadFile } from '$lib/logic/file-actions';
|
import { loadFile } from '$lib/logic/file-actions';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Map
|
<Map
|
||||||
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
||||||
accessToken={options.token}
|
maptilerKey={options.key}
|
||||||
geocoder={false}
|
geocoder={false}
|
||||||
geolocate={true}
|
geolocate={true}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
let options = $state(
|
let options = $state(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
token: 'YOUR_MAPTILER_KEY',
|
key: 'YOUR_MAPTILER_KEY',
|
||||||
theme: mode.current,
|
theme: mode.current,
|
||||||
},
|
},
|
||||||
defaultEmbeddingOptions
|
defaultEmbeddingOptions
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
let iframeOptions = $derived(
|
let iframeOptions = $derived(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
token:
|
key:
|
||||||
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY'
|
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY'
|
||||||
? PUBLIC_MAPTILER_KEY
|
? PUBLIC_MAPTILER_KEY
|
||||||
: options.key,
|
: options.key,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
} from '$lib/components/file-list/file-list';
|
} from '$lib/components/file-list/file-list';
|
||||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils';
|
||||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
||||||
import { MapPin, Square } from 'lucide-static';
|
import { MapPin, Square } from 'lucide-static';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
@@ -783,20 +783,7 @@ export class GPXLayer {
|
|||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
||||||
if (!_map.hasImage(iconId)) {
|
loadSVGIcon(_map, iconId, getSvgForSymbol(symbol, this.layerColor));
|
||||||
let icon = new Image(100, 100);
|
|
||||||
icon.onload = () => {
|
|
||||||
if (!_map.hasImage(iconId)) {
|
|
||||||
_map.addImage(iconId, icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import maplibregl from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||||
|
import { loadSVGIcon } from '$lib/utils';
|
||||||
|
|
||||||
|
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6" fill="#22c55e" stroke="white" stroke-width="1.5"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const endMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="checkerboard" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse">
|
||||||
|
<rect x="0" y="0" width="2.5" height="2.5" fill="white"/>
|
||||||
|
<rect x="2.5" y="2.5" width="2.5" height="2.5" fill="white"/>
|
||||||
|
<rect x="2.5" y="0" width="2.5" height="2.5" fill="black"/>
|
||||||
|
<rect x="0" y="2.5" width="2.5" height="2.5" fill="black"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<circle cx="8" cy="8" r="6" fill="url(#checkerboard)" stroke="white" stroke-width="1.5"/>
|
||||||
|
</svg>`;
|
||||||
|
const hoverMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6" fill="#00b8db" stroke="white" stroke-width="1.5"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
export class StartEndMarkers {
|
export class StartEndMarkers {
|
||||||
start: maplibregl.Marker;
|
|
||||||
end: maplibregl.Marker;
|
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
unsubscribes: (() => void)[] = [];
|
unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let startElement = document.createElement('div');
|
|
||||||
let endElement = document.createElement('div');
|
|
||||||
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
|
|
||||||
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
|
|
||||||
endElement.style.background =
|
|
||||||
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
|
|
||||||
|
|
||||||
this.start = new maplibregl.Marker({ element: startElement });
|
|
||||||
this.end = new maplibregl.Marker({ element: endElement });
|
|
||||||
|
|
||||||
map.onLoad(() => this.update());
|
map.onLoad(() => this.update());
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribes.push(hoveredPoint.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(allHidden.subscribe(this.updateBinded));
|
this.unsubscribes.push(allHidden.subscribe(this.updateBinded));
|
||||||
}
|
}
|
||||||
@@ -33,33 +43,113 @@ export class StartEndMarkers {
|
|||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
|
|
||||||
|
this.loadIcons();
|
||||||
|
|
||||||
const tool = get(currentTool);
|
const tool = get(currentTool);
|
||||||
const statistics = get(gpxStatistics);
|
const statistics = get(gpxStatistics);
|
||||||
const slicedStatistics = get(slicedGPXStatistics);
|
const slicedStatistics = get(slicedGPXStatistics);
|
||||||
|
const hovered = get(hoveredPoint);
|
||||||
const hidden = get(allHidden);
|
const hidden = get(allHidden);
|
||||||
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||||
this.start
|
const start = statistics
|
||||||
.setLngLat(
|
.getTrackPoint(slicedStatistics?.[1] ?? 0)!
|
||||||
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
.trkpt.getCoordinates();
|
||||||
)
|
const end = statistics
|
||||||
.addTo(map_);
|
|
||||||
this.end
|
|
||||||
.setLngLat(
|
|
||||||
statistics
|
|
||||||
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||||
.trkpt.getCoordinates()
|
.trkpt.getCoordinates();
|
||||||
)
|
const data: GeoJSON.FeatureCollection = {
|
||||||
.addTo(map_);
|
type: 'FeatureCollection',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [start.lon, start.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'start-marker',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [end.lon, end.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'end-marker',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
data.features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [hovered.lon, hovered.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'hover-marker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = map_.getSource('start-end-markers') as GeoJSONSource | undefined;
|
||||||
|
if (source) {
|
||||||
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
this.start.remove();
|
map_.addSource('start-end-markers', {
|
||||||
this.end.remove();
|
type: 'geojson',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map_.getLayer('start-end-markers')) {
|
||||||
|
map_.addLayer(
|
||||||
|
{
|
||||||
|
id: 'start-end-markers',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'start-end-markers',
|
||||||
|
layout: {
|
||||||
|
'icon-image': ['get', 'icon'],
|
||||||
|
'icon-size': 0.2,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ANCHOR_LAYER_KEY.startEndMarkers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (map_.getLayer('start-end-markers')) {
|
||||||
|
map_.removeLayer('start-end-markers');
|
||||||
|
}
|
||||||
|
if (map_.getSource('start-end-markers')) {
|
||||||
|
map_.removeSource('start-end-markers');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
this.start.remove();
|
const map_ = get(map);
|
||||||
this.end.remove();
|
if (!map_) return;
|
||||||
|
|
||||||
|
if (map_.getLayer('start-end-markers')) {
|
||||||
|
map_.removeLayer('start-end-markers');
|
||||||
|
}
|
||||||
|
if (map_.getSource('start-end-markers')) {
|
||||||
|
map_.removeSource('start-end-markers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIcons() {
|
||||||
|
const map_ = get(map);
|
||||||
|
if (!map_) return;
|
||||||
|
loadSVGIcon(map_, 'start-marker', startMarkerSVG);
|
||||||
|
loadSVGIcon(map_, 'end-marker', endMarkerSVG);
|
||||||
|
loadSVGIcon(map_, 'hover-marker', hoverMarkerSVG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { db } from '$lib/db';
|
|||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
import { ANCHOR_LAYER_KEY } from '../style';
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
import { loadSVGIcon } from '$lib/utils';
|
||||||
|
|
||||||
const { currentOverpassQueries } = settings;
|
const { currentOverpassQueries } = settings;
|
||||||
|
|
||||||
@@ -257,27 +258,16 @@ export class OverpassLayer {
|
|||||||
loadIcons() {
|
loadIcons() {
|
||||||
let currentQueries = getCurrentQueries();
|
let currentQueries = getCurrentQueries();
|
||||||
currentQueries.forEach((query) => {
|
currentQueries.forEach((query) => {
|
||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
loadSVGIcon(
|
||||||
let icon = new Image(100, 100);
|
this.map,
|
||||||
icon.onload = () => {
|
`overpass-${query}`,
|
||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
this.map.addImage(`overpass-${query}`, icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(`
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
|
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>`
|
||||||
`);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const ANCHOR_LAYER_KEY = {
|
|||||||
tracks: 'tracks-end',
|
tracks: 'tracks-end',
|
||||||
directionMarkers: 'direction-markers-end',
|
directionMarkers: 'direction-markers-end',
|
||||||
distanceMarkers: 'distance-markers-end',
|
distanceMarkers: 'distance-markers-end',
|
||||||
|
startEndMarkers: 'start-end-markers-end',
|
||||||
interactions: 'interactions-end',
|
interactions: 'interactions-end',
|
||||||
overpass: 'overpass-end',
|
overpass: 'overpass-end',
|
||||||
waypoints: 'waypoints-end',
|
waypoints: 'waypoints-end',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
|||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
import { loadSVGIcon } from '$lib/utils';
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
@@ -24,28 +25,16 @@ export class SplitControls {
|
|||||||
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.layerEventManager = layerEventManager;
|
this.layerEventManager = layerEventManager;
|
||||||
|
loadSVGIcon(
|
||||||
if (!this.map.hasImage('split-control')) {
|
this.map,
|
||||||
let icon = new Image(100, 100);
|
'split-control',
|
||||||
icon.onload = () => {
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
if (!this.map.hasImage('split-control')) {
|
|
||||||
this.map.addImage('split-control', icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(`
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="white" />
|
<circle cx="20" cy="20" r="20" fill="white" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>`
|
||||||
`);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ title: Files and statistics
|
|||||||
|
|
||||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||||
let slicedGPXStatistics = writable(undefined);
|
let slicedGPXStatistics = writable(undefined);
|
||||||
|
let hoveredPoint = writable(null);
|
||||||
let additionalDatasets = writable(['speed', 'atemp']);
|
let additionalDatasets = writable(['speed', 'atemp']);
|
||||||
let elevationFill = writable(undefined);
|
let elevationFill = writable(undefined);
|
||||||
</script>
|
</script>
|
||||||
@@ -84,6 +85,7 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx';
|
||||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -82,6 +82,8 @@ export const gpxStatistics = new SelectedGPXStatistics();
|
|||||||
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
||||||
writable(undefined);
|
writable(undefined);
|
||||||
|
|
||||||
|
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
|
||||||
|
|
||||||
gpxStatistics.subscribe(() => {
|
gpxStatistics.subscribe(() => {
|
||||||
slicedGPXStatistics.set(undefined);
|
slicedGPXStatistics.set(undefined);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -197,6 +197,18 @@ export function getElevation(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string) {
|
||||||
|
if (!map.hasImage(id)) {
|
||||||
|
let icon = new Image(100, 100);
|
||||||
|
icon.onload = () => {
|
||||||
|
if (!map.hasImage(id)) {
|
||||||
|
map.addImage(id, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isMac() {
|
export function isMac() {
|
||||||
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||||
let slicedGPXStatistics = writable(undefined);
|
let slicedGPXStatistics = writable(undefined);
|
||||||
|
let hoveredPoint = writable(null);
|
||||||
let additionalDatasets = writable(['speed', 'atemp']);
|
let additionalDatasets = writable(['speed', 'atemp']);
|
||||||
let elevationFill = writable(undefined);
|
let elevationFill = writable(undefined);
|
||||||
|
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { loadFiles } from '$lib/logic/file-actions';
|
import { loadFiles } from '$lib/logic/file-actions';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
@@ -140,6 +140,7 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user