11 Commits

Author SHA1 Message Date
vcoppe
4ea0ea6a7a update mapbox 2025-11-20 00:27:53 +01:00
vcoppe
2e3ce83605 fix null check 2025-11-20 00:25:56 +01:00
vcoppe
fda908dd0d listen to touchstart event on layer 2025-11-19 23:45:28 +01:00
vcoppe
cad77e2b10 try fix dragging on touch devices 2025-11-19 23:36:02 +01:00
vcoppe
3542a7c24d New translations en.json (Polish) (#273) 2025-11-19 23:01:01 +01:00
vcoppe
0d6d161e23 add missing keyboard navigation, closes #277 2025-11-19 23:00:33 +01:00
vcoppe
89a2e0086b preview new POI 2025-11-19 22:43:19 +01:00
vcoppe
cd443faf61 cancel drag on click 2025-11-19 22:28:40 +01:00
vcoppe
bfc56b02a8 use map layer instead of markers for POIs 2025-11-19 21:59:17 +01:00
vcoppe
25bafc6bf1 improve bounds filtering 2025-11-17 22:53:48 +01:00
vcoppe
6387580626 speed up split controls 2025-11-16 16:46:31 +01:00
14 changed files with 746 additions and 381 deletions

View File

@@ -22,7 +22,7 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.12.0", "mapbox-gl": "^3.16.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
@@ -1701,9 +1701,10 @@
} }
}, },
"node_modules/@mapbox/point-geometry": { "node_modules/@mapbox/point-geometry": {
"version": "0.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
"license": "ISC"
}, },
"node_modules/@mapbox/polyline": { "node_modules/@mapbox/polyline": {
"version": "1.2.1", "version": "1.2.1",
@@ -1738,11 +1739,26 @@
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
}, },
"node_modules/@mapbox/vector-tile": { "node_modules/@mapbox/vector-tile": {
"version": "1.3.1", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@mapbox/point-geometry": "~0.1.0" "@mapbox/point-geometry": "~1.1.0",
"@types/geojson": "^7946.0.16",
"pbf": "^4.0.1"
}
},
"node_modules/@mapbox/vector-tile/node_modules/pbf": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
} }
}, },
"node_modules/@mapbox/whoots-js": { "node_modules/@mapbox/whoots-js": {
@@ -2644,7 +2660,8 @@
"node_modules/@types/mapbox__point-geometry": { "node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==" "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
"license": "MIT"
}, },
"node_modules/@types/mapbox__sphericalmercator": { "node_modules/@types/mapbox__sphericalmercator": {
"version": "1.2.3", "version": "1.2.3",
@@ -2660,16 +2677,6 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/mapbox__vector-tile": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox__point-geometry": "*",
"@types/pbf": "*"
}
},
"node_modules/@types/mapbox-gl": { "node_modules/@types/mapbox-gl": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
@@ -4947,9 +4954,10 @@
} }
}, },
"node_modules/gl-matrix": { "node_modules/gl-matrix": {
"version": "3.4.3", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
"license": "MIT"
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "11.0.2", "version": "11.0.2",
@@ -6061,9 +6069,9 @@
} }
}, },
"node_modules/mapbox-gl": { "node_modules/mapbox-gl": {
"version": "3.12.0", "version": "3.16.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.12.0.tgz", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz",
"integrity": "sha512-DV6TRr+xoPrLSKuGiUcbyLVkoLdNaNNpn6O7+ZC27yQH7BOOIF7l6JKbTCMhfMJuZBVJfL8YRJjlMJ6MZCTggA==", "integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==",
"license": "SEE LICENSE IN LICENSE.txt", "license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [ "workspaces": [
"src/style-spec", "src/style-spec",
@@ -6072,33 +6080,43 @@
"dependencies": { "dependencies": {
"@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/mapbox-gl-supported": "^3.0.0",
"@mapbox/point-geometry": "^0.1.0", "@mapbox/point-geometry": "^1.1.0",
"@mapbox/tiny-sdf": "^2.0.6", "@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1", "@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^1.3.1", "@mapbox/vector-tile": "^2.0.4",
"@mapbox/whoots-js": "^3.1.0", "@mapbox/whoots-js": "^3.1.0",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/geojson-vt": "^3.2.5", "@types/geojson-vt": "^3.2.5",
"@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__point-geometry": "^0.1.4",
"@types/mapbox__vector-tile": "^1.3.4",
"@types/pbf": "^3.0.5", "@types/pbf": "^3.0.5",
"@types/supercluster": "^7.1.3", "@types/supercluster": "^7.1.3",
"cheap-ruler": "^4.0.0", "cheap-ruler": "^4.0.0",
"csscolorparser": "~1.0.3", "csscolorparser": "~1.0.3",
"earcut": "^3.0.1", "earcut": "^3.0.1",
"geojson-vt": "^4.0.2", "geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.3", "gl-matrix": "^3.4.4",
"grid-index": "^1.1.0", "grid-index": "^1.1.0",
"kdbush": "^4.0.2", "kdbush": "^4.0.2",
"martinez-polygon-clipping": "^0.7.4", "martinez-polygon-clipping": "^0.7.4",
"murmurhash-js": "^1.0.0", "murmurhash-js": "^1.0.0",
"pbf": "^3.2.1", "pbf": "^4.0.1",
"potpack": "^2.0.0", "potpack": "^2.0.0",
"quickselect": "^3.0.0", "quickselect": "^3.0.0",
"serialize-to-js": "^3.1.2", "serialize-to-js": "^3.1.2",
"supercluster": "^8.0.1", "supercluster": "^8.0.1",
"tinyqueue": "^3.0.0", "tinyqueue": "^3.0.0"
"vt-pbf": "^3.1.3" }
},
"node_modules/mapbox-gl/node_modules/pbf": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
} }
}, },
"node_modules/mapillary-js": { "node_modules/mapillary-js": {
@@ -9021,16 +9039,6 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"dev": true "dev": true
}, },
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
"@mapbox/vector-tile": "^1.3.1",
"pbf": "^3.2.1"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -74,7 +74,7 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.12.0", "mapbox-gl": "^3.16.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",

View File

@@ -644,6 +644,19 @@
} else if (e.key === 'F5') { } else if (e.key === 'F5') {
$routing = !$routing; $routing = !$routing;
e.preventDefault(); e.preventDefault();
} else if (
e.key === 'ArrowRight' ||
e.key === 'ArrowDown' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowUp'
) {
if (!targetInput) {
selection.updateFromKey(
e.key === 'ArrowRight' || e.key === 'ArrowDown',
e.shiftKey
);
e.preventDefault();
}
} }
}} }}
on:dragover={(e) => e.preventDefault()} on:dragover={(e) => e.preventDefault()}

View File

@@ -175,7 +175,7 @@
let file = fileStateCollection.getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (layer && file) { if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint && !waypoint._data.hidden) {
waypointPopup?.setItem({ waypointPopup?.setItem({
item: waypoint, item: waypoint,
fileId: item.getFileId(), fileId: item.getFileId(),

View File

@@ -13,6 +13,8 @@
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import type { PopupItem } from '$lib/components/map/map-popup'; import type { PopupItem } from '$lib/components/map/map-popup';
import { selection } from '$lib/logic/selection';
import { ListFileItem } from '$lib/components/file-list/file-list';
let { let {
waypoint, waypoint,
@@ -20,6 +22,9 @@
waypoint: PopupItem<Waypoint>; waypoint: PopupItem<Waypoint>;
} = $props(); } = $props();
let selected = $derived(
waypoint.fileId ? $selection.hasAnyChildren(new ListFileItem(waypoint.fileId)) : false
);
let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined); let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
function sanitize(text: string | undefined): string { function sanitize(text: string | undefined): string {
@@ -81,7 +86,7 @@
</ScrollArea> </ScrollArea>
<div class="mt-2 flex flex-col gap-1"> <div class="mt-2 flex flex-col gap-1">
<CopyCoordinates coordinates={waypoint.item.attributes} /> <CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT} {#if $currentTool === Tool.WAYPOINT && selected}
<Button <Button
class="p-1 has-[>svg]:px-2 h-8" class="p-1 has-[>svg]:px-2 h-8"
variant="outline" variant="outline"

View File

@@ -8,14 +8,7 @@ import { allHidden } from '$lib/logic/hidden';
const { distanceMarkers, distanceUnits } = settings; const { distanceMarkers, distanceUnits } = settings;
const stops = [ const levels = [100, 50, 25, 10, 5, 1];
[100, 0],
[50, 7],
[25, 8, 10],
[10, 10],
[5, 11],
[1, 13],
];
export class DistanceMarkers { export class DistanceMarkers {
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
@@ -50,43 +43,50 @@ export class DistanceMarkers {
data: this.getDistanceMarkersGeoJSON(), data: this.getDistanceMarkersGeoJSON(),
}); });
} }
stops.forEach(([d, minzoom, maxzoom]) => { if (!map_.getLayer('distance-markers')) {
if (!map_.getLayer(`distance-markers-${d}`)) { map_.addLayer({
map_.addLayer({ id: 'distance-markers',
id: `distance-markers-${d}`, type: 'symbol',
type: 'symbol', source: 'distance-markers',
source: 'distance-markers', filter: [
filter: 'match',
d === 5 ['get', 'level'],
? [ 100,
'any', ['>=', ['zoom'], 0],
['==', ['get', 'level'], 5], 50,
['==', ['get', 'level'], 25], ['>=', ['zoom'], 7],
] 25,
: ['==', ['get', 'level'], d], [
minzoom: minzoom, 'any',
maxzoom: maxzoom ?? 24, ['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
layout: { ['>=', ['zoom'], 11],
'text-field': ['get', 'distance'], ],
'text-size': 14, 10,
'text-font': ['Open Sans Bold'], ['>=', ['zoom'], 10],
}, 5,
paint: { ['>=', ['zoom'], 11],
'text-color': 'black', 1,
'text-halo-width': 2, ['>=', ['zoom'], 13],
'text-halo-color': 'white', false,
}, ],
}); layout: {
} else { 'text-field': ['get', 'distance'],
map_.moveLayer(`distance-markers-${d}`); 'text-size': 14,
} 'text-font': ['Open Sans Bold'],
}); },
paint: {
'text-color': 'black',
'text-halo-width': 2,
'text-halo-color': 'white',
},
});
} else {
map_.moveLayer('distance-markers');
}
} else { } else {
stops.forEach(([d]) => { if (map_.getLayer('distance-markers')) {
if (map_.getLayer(`distance-markers-${d}`)) { map_.removeLayer('distance-markers');
map_.removeLayer(`distance-markers-${d}`); }
}
});
} }
} 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
@@ -109,9 +109,7 @@ export class DistanceMarkers {
getConvertedDistanceToKilometers(currentTargetDistance) getConvertedDistanceToKilometers(currentTargetDistance)
) { ) {
let distance = currentTargetDistance.toFixed(0); let distance = currentTargetDistance.toFixed(0);
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [ let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
0, 0,
];
features.push({ features.push({
type: 'Feature', type: 'Feature',
geometry: { geometry: {
@@ -124,7 +122,6 @@ export class DistanceMarkers {
properties: { properties: {
distance, distance,
level, level,
minzoom,
}, },
} as GeoJSON.Feature); } as GeoJSON.Feature);
currentTargetDistance += 1; currentTargetDistance += 1;

View File

@@ -55,14 +55,18 @@ function decrementColor(color: string) {
} }
} }
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) { export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined; let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square.replace('width="24"', 'width="12"') ${
.replace('height="24"', 'height="12"') layerColor
.replace('stroke="currentColor"', 'stroke="SteelBlue"') ? Square.replace('width="24"', 'width="12"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"') .replace('height="24"', 'height="12"')
.replace('fill="none"', `fill="${layerColor}"`)} .replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)
: ''
}
${MapPin.replace('width="24"', '') ${MapPin.replace('width="24"', '')
.replace('height="24"', '') .replace('height="24"', '')
.replace('stroke="currentColor"', '') .replace('stroke="currentColor"', '')
@@ -87,9 +91,10 @@ export class GPXLayer {
fileId: string; fileId: string;
file: Readable<GPXFileWithStatistics | undefined>; file: Readable<GPXFileWithStatistics | undefined>;
layerColor: string; layerColor: string;
markers: mapboxgl.Marker[] = [];
selected: boolean = false; selected: boolean = false;
draggable: boolean; currentWaypointData: GeoJSON.FeatureCollection | null = null;
draggedWaypointIndex: number | null = null;
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
unsubscribe: Function[] = []; unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
@@ -98,6 +103,20 @@ export class GPXLayer {
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this); layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this); layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseEnter.bind(this);
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseLeave.bind(this);
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnClick.bind(this);
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseDown.bind(this);
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnTouchStart.bind(this);
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseMove.bind(this);
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseUp.bind(this);
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) { constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
this.fileId = fileId; this.fileId = fileId;
@@ -125,18 +144,6 @@ export class GPXLayer {
}) })
); );
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(
currentTool.subscribe((tool) => {
if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true;
this.markers.forEach((marker) => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false;
this.markers.forEach((marker) => marker.setDraggable(false));
}
})
);
this.draggable = get(currentTool) === Tool.WAYPOINT;
} }
update() { update() {
@@ -146,6 +153,8 @@ export class GPXLayer {
return; return;
} }
this.loadIcons();
if ( if (
file._data.style && file._data.style &&
file._data.style.color && file._data.style.color &&
@@ -189,6 +198,56 @@ export class GPXLayer {
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded); _map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
} }
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| undefined;
this.currentWaypointData = this.getWaypointsGeoJSON();
if (waypointSource) {
waypointSource.setData(this.currentWaypointData);
} else {
_map.addSource(this.fileId + '-waypoints', {
type: 'geojson',
data: this.currentWaypointData,
});
}
if (!_map.getLayer(this.fileId + '-waypoints')) {
_map.addLayer({
id: this.fileId + '-waypoints',
type: 'symbol',
source: this.fileId + '-waypoints',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.3,
'icon-anchor': 'bottom',
'icon-padding': 0,
'icon-allow-overlap': true,
},
});
_map.on(
'mouseenter',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.on(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.on(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
_map.on(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
}
if (get(directionMarkers)) { if (get(directionMarkers)) {
if (!_map.getLayer(this.fileId + '-direction')) { if (!_map.getLayer(this.fileId + '-direction')) {
_map.addLayer( _map.addLayer(
@@ -213,7 +272,7 @@ export class GPXLayer {
'text-halo-color': 'white', 'text-halo-color': 'white',
}, },
}, },
_map.getLayer('distance-markers-100') ? 'distance-markers-100' : undefined _map.getLayer('distance-markers') ? 'distance-markers' : undefined
); );
} }
} else { } else {
@@ -222,10 +281,10 @@ export class GPXLayer {
} }
} }
let visibleItems: [number, number][] = []; let visibleSegments: [number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) { if (!segment._data.hidden) {
visibleItems.push([trackIndex, segmentIndex]); visibleSegments.push([trackIndex, segmentIndex]);
} }
}); });
@@ -233,7 +292,7 @@ export class GPXLayer {
this.fileId, this.fileId,
[ [
'any', 'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [ ...visibleSegments.map(([trackIndex, segmentIndex]) => [
'all', 'all',
['==', 'trackIndex', trackIndex], ['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex], ['==', 'segmentIndex', segmentIndex],
@@ -241,12 +300,26 @@ export class GPXLayer {
], ],
{ validate: false } { validate: false }
); );
let visibleWaypoints: number[] = [];
file.wpt.forEach((waypoint, waypointIndex) => {
if (!waypoint._data.hidden) {
visibleWaypoints.push(waypointIndex);
}
});
_map.setFilter(
this.fileId + '-waypoints',
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]],
{ validate: false }
);
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.setFilter( _map.setFilter(
this.fileId + '-direction', this.fileId + '-direction',
[ [
'any', 'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [ ...visibleSegments.map(([trackIndex, segmentIndex]) => [
'all', 'all',
['==', 'trackIndex', trackIndex], ['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex], ['==', 'segmentIndex', segmentIndex],
@@ -259,114 +332,6 @@ export class GPXLayer {
// 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
return; return;
} }
let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => {
// Update markers
let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) {
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
symbolKey,
this.layerColor
);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
value: waypoint,
writable: true,
});
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
let marker = new mapboxgl.Marker({
draggable: this.draggable,
element,
anchor: 'bottom',
}).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0;
marker.getElement().addEventListener('mousemove', (e) => {
if (marker._isDragging) {
return;
}
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
e.stopPropagation();
});
marker.getElement().addEventListener('click', (e) => {
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
return;
}
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
fileActions.deleteWaypoint(this.fileId, marker._waypoint._data.index);
e.stopPropagation();
return;
}
if (get(treeFileView)) {
if (
(e.ctrlKey || e.metaKey) &&
get(selection).hasAnyChildren(
new ListWaypointsItem(this.fileId),
false
)
) {
selection.addSelectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} else {
selection.selectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
}
} else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]);
} else {
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
}
e.stopPropagation();
});
marker.on('dragstart', () => {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
marker.getElement().style.cursor = 'grabbing';
waypointPopup?.hide();
});
marker.on('dragend', (e) => {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => {
fileActionManager.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng,
});
wpt.ele = ele[0];
});
});
dragEndTimestamp = Date.now();
});
this.markers.push(marker);
}
markerIndex++;
});
}
while (markerIndex < this.markers.length) {
// Remove extra markers
this.markers.pop()?.remove();
}
this.markers.forEach((marker) => {
if (!marker._waypoint._data.hidden) {
marker.addTo(_map);
} else {
marker.remove();
}
});
} }
remove() { remove() {
@@ -379,6 +344,24 @@ export class GPXLayer {
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded); _map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
_map.off('style.import.load', this.updateBinded); _map.off('style.import.load', this.updateBinded);
_map.off(
'mouseenter',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.off(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
_map.off(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction'); _map.removeLayer(this.fileId + '-direction');
} }
@@ -388,12 +371,14 @@ export class GPXLayer {
if (_map.getSource(this.fileId)) { if (_map.getSource(this.fileId)) {
_map.removeSource(this.fileId); _map.removeSource(this.fileId);
} }
if (_map.getLayer(this.fileId + '-waypoints')) {
_map.removeLayer(this.fileId + '-waypoints');
}
if (_map.getSource(this.fileId + '-waypoints')) {
_map.removeSource(this.fileId + '-waypoints');
}
} }
this.markers.forEach((marker) => {
marker.remove();
});
this.unsubscribe.forEach((unsubscribe) => unsubscribe()); this.unsubscribe.forEach((unsubscribe) => unsubscribe());
decrementColor(this.layerColor); decrementColor(this.layerColor);
@@ -407,6 +392,9 @@ export class GPXLayer {
if (_map.getLayer(this.fileId)) { if (_map.getLayer(this.fileId)) {
_map.moveLayer(this.fileId); _map.moveLayer(this.fileId);
} }
if (_map.getLayer(this.fileId + '-waypoints')) {
_map.moveLayer(this.fileId + '-waypoints');
}
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.moveLayer(this.fileId + '-direction'); _map.moveLayer(this.fileId + '-direction');
} }
@@ -449,7 +437,7 @@ export class GPXLayer {
} }
} }
layerOnClick(e: any) { layerOnClick(e: mapboxgl.MapMouseEvent) {
if ( if (
get(currentTool) === Tool.ROUTING && get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -457,8 +445,8 @@ export class GPXLayer {
return; return;
} }
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features![0].properties!.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features![0].properties!.segmentIndex;
if ( if (
get(currentTool) === Tool.SCISSORS && get(currentTool) === Tool.SCISSORS &&
@@ -466,6 +454,11 @@ export class GPXLayer {
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) )
) { ) {
if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) {
// Clicked on split control, ignoring
return;
}
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, { fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng, lon: e.lngLat.lng,
@@ -502,6 +495,160 @@ export class GPXLayer {
} }
} }
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
if (this.draggedWaypointIndex !== null) {
return;
}
let file = get(this.file)?.file;
if (!file) {
return;
}
let waypointIndex = e.features![0].properties!.waypointIndex;
let waypoint = file.wpt[waypointIndex];
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true);
}
waypointLayerOnMouseLeave() {
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
}
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
e.preventDefault();
let waypointIndex = e.features![0].properties!.waypointIndex;
let file = get(this.file)?.file;
if (!file) {
return;
}
let waypoint = file.wpt[waypointIndex];
if (get(currentTool) === Tool.WAYPOINT) {
if (this.selected) {
if (e.originalEvent.shiftKey) {
fileActions.deleteWaypoint(this.fileId, waypointIndex);
} else {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
selectedWaypoint.set([waypoint, this.fileId]);
}
} else {
if (get(treeFileView)) {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
} else {
selection.selectItem(new ListFileItem(this.fileId));
}
selectedWaypoint.set([waypoint, this.fileId]);
}
} else {
if (get(treeFileView)) {
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.selected) {
selection.addSelectItem(new ListWaypointItem(this.fileId, waypointIndex));
} else {
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
}
} else {
if (!this.selected) {
selection.selectItem(new ListFileItem(this.fileId));
}
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
}
}
}
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
const _map = get(map);
if (!_map) {
return;
}
e.preventDefault();
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point;
waypointPopup?.hide();
_map.on('mousemove', this.waypointLayerOnMouseMoveBinded);
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
const _map = get(map);
if (!_map) {
return;
}
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point;
waypointPopup?.hide();
e.preventDefault();
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
return;
}
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
(
this.currentWaypointData!.features[this.draggedWaypointIndex].geometry as GeoJSON.Point
).coordinates = [e.lngLat.lng, e.lngLat.lat];
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| undefined;
if (waypointSource) {
waypointSource.setData(this.currentWaypointData!);
}
}
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
if (this.draggedWaypointIndex === null) {
return;
}
if (e.point.equals(this.draggingStartingPosition)) {
this.draggedWaypointIndex = null;
return;
}
getElevation([
{
lat: e.lngLat.lat,
lon: e.lngLat.lng,
},
]).then((ele) => {
if (this.draggedWaypointIndex === null) {
return;
}
fileActionManager.applyToFile(this.fileId, (file) => {
let wpt = file.wpt[this.draggedWaypointIndex!];
wpt.setCoordinates({
lat: e.lngLat.lat,
lon: e.lngLat.lng,
});
wpt.ele = ele[0];
});
this.draggedWaypointIndex = null;
});
}
getGeoJSON(): GeoJSON.FeatureCollection { getGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file; let file = get(this.file)?.file;
if (!file) { if (!file) {
@@ -548,4 +695,65 @@ export class GPXLayer {
} }
return data; return data;
} }
getWaypointsGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
if (!file) {
return data;
}
file.wpt.forEach((waypoint, index) => {
data.features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [waypoint.getLongitude(), waypoint.getLatitude()],
},
properties: {
fileId: this.fileId,
waypointIndex: index,
icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`,
},
});
});
return data;
}
loadIcons() {
const _map = get(map);
let file = get(this.file)?.file;
if (!_map || !file) {
return;
}
let symbols = new Set<string | undefined>();
file.wpt.forEach((waypoint) => {
symbols.add(getSymbolKey(waypoint.sym));
});
symbols.forEach((symbol) => {
const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`;
if (!_map.hasImage(iconId)) {
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));
}
});
}
} }

View File

@@ -101,7 +101,9 @@ export class OverpassLayer {
this.map.on('click', 'overpass', this.onHoverBinded); this.map.on('click', 'overpass', this.onHoverBinded);
} }
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]); this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
validate: false,
});
} 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
} }

View File

@@ -1,5 +1,3 @@
import { TrackPoint, TrackSegment } from 'gpx';
import mapboxgl from 'mapbox-gl';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { currentTool, Tool } from '$lib/components/toolbar/tools'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
@@ -9,20 +7,41 @@ import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store'; 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';
export class SplitControls { export class SplitControls {
active: boolean = false;
map: mapboxgl.Map; map: mapboxgl.Map;
controls: ControlWithMarker[] = [];
shownControls: ControlWithMarker[] = [];
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
toggleControlsForZoomLevelAndBoundsBinded: () => void = layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
this.toggleControlsForZoomLevelAndBounds.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
if (!this.map.hasImage('split-control')) {
let icon = new Image(100, 100);
icon.onload = () => {
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" />
<g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g>
</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)));
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
@@ -31,29 +50,18 @@ export class SplitControls {
addIfNeeded() { addIfNeeded() {
let scissors = get(currentTool) === Tool.SCISSORS; let scissors = get(currentTool) === Tool.SCISSORS;
if (!scissors) { if (!scissors) {
if (this.active) { this.remove();
this.remove();
}
return; return;
} }
if (this.active) { this.updateControls();
this.updateControls();
} else {
this.add();
}
}
add() {
this.active = true;
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
updateControls() { updateControls() {
// Update the markers when the files change let data: GeoJSON.FeatureCollection = {
let controlIndex = 0; type: 'FeatureCollection',
features: [],
};
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = fileStateCollection.getFile(fileId); let file = fileStateCollection.getFile(fileId);
@@ -64,30 +72,23 @@ export class SplitControls {
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
) )
) { ) {
for (let point of segment.trkpt.slice(1, -1)) { for (let i = 1; i < segment.trkpt.length - 1; i++) {
// Update the existing controls (could be improved by matching the existing controls with the new ones?) let point = segment.trkpt[i];
if (point._data.anchor) { if (point._data.anchor) {
if (controlIndex < this.controls.length) { data.features.push({
this.controls[controlIndex].fileId = fileId; type: 'Feature',
this.controls[controlIndex].point = point; geometry: {
this.controls[controlIndex].segment = segment; type: 'Point',
this.controls[controlIndex].trackIndex = trackIndex; coordinates: [point.getLongitude(), point.getLatitude()],
this.controls[controlIndex].segmentIndex = segmentIndex; },
this.controls[controlIndex].marker.setLngLat( properties: {
point.getCoordinates() fileId: fileId,
); trackIndex: trackIndex,
} else { segmentIndex: segmentIndex,
this.controls.push( pointIndex: i,
this.createControl( minZoom: point._data.zoom,
point, },
segment, });
fileId,
trackIndex,
segmentIndex
)
);
}
controlIndex++;
} }
} }
} }
@@ -95,86 +96,77 @@ export class SplitControls {
} }
}, false); }, false);
while (controlIndex < this.controls.length) { try {
// Remove the extra controls let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
this.controls.pop()?.marker.remove(); if (source) {
} source.setData(data);
} else {
this.map.addSource('split-controls', {
type: 'geojson',
data: data,
});
}
this.toggleControlsForZoomLevelAndBounds(); if (!this.map.getLayer('split-controls')) {
this.map.addLayer({
id: 'split-controls',
type: 'symbol',
source: 'split-controls',
layout: {
'icon-image': 'split-control',
'icon-size': 0.25,
'icon-padding': 0,
},
filter: ['<=', ['get', 'minZoom'], ['zoom']],
});
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.on('click', 'split-controls', this.layerOnClickBinded);
}
this.map.moveLayer('split-controls');
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
} }
remove() { remove() {
this.active = false; this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.off('click', 'split-controls', this.layerOnClickBinded);
for (let control of this.controls) { try {
control.marker.remove(); if (this.map.getLayer('split-controls')) {
} this.map.removeLayer('split-controls');
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
}
toggleControlsForZoomLevelAndBounds() {
// Show markers only if they are in the current zoom level and bounds
this.shownControls.splice(0, this.shownControls.length);
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
let zoom = this.map.getZoom();
this.controls.forEach((control) => {
control.inZoom = control.point._data.zoom <= zoom;
if (control.inZoom && bounds.contains(control.marker.getLngLat())) {
control.marker.addTo(this.map);
this.shownControls.push(control);
} else {
control.marker.remove();
} }
});
if (this.map.getSource('split-controls')) {
this.map.removeSource('split-controls');
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
} }
createControl( layerOnMouseEnter(e: any) {
point: TrackPoint, mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
segment: TrackSegment, }
fileId: string,
trackIndex: number,
segmentIndex: number
): ControlWithMarker {
let element = document.createElement('div');
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
element.innerHTML = Scissors.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', 'stroke="black"');
let marker = new mapboxgl.Marker({ layerOnMouseLeave() {
draggable: true, mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
className: 'z-10', }
element,
}).setLngLat(point.getCoordinates());
let control = { layerOnClick(e: mapboxgl.MapMouseEvent) {
point, let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
segment, fileActions.split(
fileId, get(splitAs),
trackIndex, e.features![0].properties!.fileId,
segmentIndex, e.features![0].properties!.trackIndex,
marker, e.features![0].properties!.segmentIndex,
inZoom: false, { lon: coordinates[0], lat: coordinates[1] },
}; e.features![0].properties!.pointIndex
);
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
fileActions.split(
get(splitAs),
control.fileId,
control.trackIndex,
control.segmentIndex,
control.point.getCoordinates(),
control.point._data.index
);
});
return control;
} }
destroy() { destroy() {
@@ -182,16 +174,3 @@ export class SplitControls {
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
} }
} }
type Control = {
segment: TrackSegment;
fileId: string;
trackIndex: number;
segmentIndex: number;
point: TrackPoint;
};
type ControlWithMarker = Control & {
marker: mapboxgl.Marker;
inZoom: boolean;
};

View File

@@ -16,6 +16,8 @@
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import mapboxgl from 'mapbox-gl';
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
let props: { let props: {
class?: string; class?: string;
@@ -39,6 +41,21 @@
}) })
); );
let marker: mapboxgl.Marker | null = null;
function reset() {
if ($selectedWaypoint) {
selectedWaypoint.reset();
} else {
name = '';
description = '';
link = '';
sym = '';
longitude = 0;
latitude = 0;
}
}
$effect(() => { $effect(() => {
if ($selectedWaypoint) { if ($selectedWaypoint) {
const wpt = $selectedWaypoint[0]; const wpt = $selectedWaypoint[0];
@@ -54,14 +71,7 @@
latitude = parseFloat(wpt.getLatitude().toFixed(6)); latitude = parseFloat(wpt.getLatitude().toFixed(6));
}); });
} else { } else {
untrack(() => { untrack(reset);
name = '';
description = '';
link = '';
sym = '';
longitude = 0;
latitude = 0;
});
} }
}); });
@@ -92,7 +102,7 @@
: undefined : undefined
); );
selectedWaypoint.reset(); reset();
} }
function setCoordinates(e: any) { function setCoordinates(e: any) {
@@ -100,6 +110,37 @@
longitude = e.lngLat.lng.toFixed(6); longitude = e.lngLat.lng.toFixed(6);
} }
$effect(() => {
if ($selectedWaypoint) {
if (marker) {
marker.remove();
marker = null;
}
} else if (latitude != 0 || longitude != 0) {
if ($map) {
if (marker) {
marker.setLngLat([longitude, latitude]).getElement().innerHTML =
getSvgForSymbol(symbolKey);
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8');
element.innerHTML = getSvgForSymbol(symbolKey);
marker = new mapboxgl.Marker({
element,
anchor: 'bottom',
})
.setLngLat([longitude, latitude])
.addTo($map);
}
}
} else {
if (marker) {
marker.remove();
marker = null;
}
}
});
onMount(() => { onMount(() => {
if ($map) { if ($map) {
$map.on('click', setCoordinates); $map.on('click', setCoordinates);
@@ -112,6 +153,10 @@
$map.off('click', setCoordinates); $map.off('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false); mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
} }
if (marker) {
marker.remove();
marker = null;
}
}); });
</script> </script>
@@ -210,7 +255,7 @@
{i18n._('toolbar.waypoint.create')} {i18n._('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}> <Button variant="outline" size="icon" onclick={reset}>
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>

View File

@@ -66,10 +66,8 @@ export class BoundsManager {
finalizeFitBounds() { finalizeFitBounds() {
if ( if (
this._bounds.getSouth() === 90 && this._bounds.getSouth() >= this._bounds.getNorth() &&
this._bounds.getWest() === 180 && this._bounds.getWest() >= this._bounds.getEast()
this._bounds.getNorth() === -90 &&
this._bounds.getEast() === -180
) { ) {
return; return;
} }

View File

@@ -4,10 +4,12 @@ import { get, writable, type Writable } from 'svelte/store';
export enum MapCursorState { export enum MapCursorState {
DEFAULT, DEFAULT,
LAYER_HOVER, LAYER_HOVER,
TOOL_WITH_CROSSHAIR,
WAYPOINT_HOVER,
WAYPOINT_DRAGGING, WAYPOINT_DRAGGING,
TRACKPOINT_DRAGGING, TRACKPOINT_DRAGGING,
TOOL_WITH_CROSSHAIR,
SCISSORS, SCISSORS,
SPLIT_CONTROL,
MAPILLARY_HOVER, MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR, STREET_VIEW_CROSSHAIR,
} }
@@ -16,10 +18,12 @@ const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/20
const cursorStyles = { const cursorStyles = {
[MapCursorState.DEFAULT]: 'default', [MapCursorState.DEFAULT]: 'default',
[MapCursorState.LAYER_HOVER]: 'pointer', [MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing', [MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing', [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair', [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor, [MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.SPLIT_CONTROL]: 'pointer',
[MapCursorState.MAPILLARY_HOVER]: 'pointer', [MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair', [MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
}; };

View File

@@ -179,6 +179,112 @@ export class Selection {
} }
} }
updateFromKey(down: boolean, shift: boolean) {
let selected = get(this._selection).getSelected();
if (selected.length === 0) {
return;
}
let next: ListItem | undefined = undefined;
if (selected[0] instanceof ListFileItem) {
let order = get(settings.fileOrder);
let limitIndex: number | undefined = undefined;
selected.forEach((item) => {
let index = order.indexOf(item.getFileId());
if (
limitIndex === undefined ||
(down && index > limitIndex) ||
(!down && index < limitIndex)
) {
limitIndex = index;
}
});
if (limitIndex !== undefined) {
let nextIndex = down ? limitIndex + 1 : limitIndex - 1;
while (true) {
if (nextIndex < 0) {
nextIndex = order.length - 1;
} else if (nextIndex >= order.length) {
nextIndex = 0;
}
if (nextIndex === limitIndex) {
break;
}
next = new ListFileItem(order[nextIndex]);
if (!get(selection).has(next)) {
break;
}
nextIndex += down ? 1 : -1;
}
}
} else if (
selected[0] instanceof ListTrackItem &&
selected[selected.length - 1] instanceof ListTrackItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let numberOfTracks = file.trk.length;
let trackIndex = down
? selected[selected.length - 1].getTrackIndex()
: selected[0].getTrackIndex();
if (down && trackIndex < numberOfTracks - 1) {
next = new ListTrackItem(fileId, trackIndex + 1);
} else if (!down && trackIndex > 0) {
next = new ListTrackItem(fileId, trackIndex - 1);
}
}
} else if (
selected[0] instanceof ListTrackSegmentItem &&
selected[selected.length - 1] instanceof ListTrackSegmentItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let trackIndex = selected[0].getTrackIndex();
let numberOfSegments = file.trk[trackIndex].trkseg.length;
let segmentIndex = down
? selected[selected.length - 1].getSegmentIndex()
: selected[0].getSegmentIndex();
if (down && segmentIndex < numberOfSegments - 1) {
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1);
} else if (!down && segmentIndex > 0) {
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1);
}
}
} else if (
selected[0] instanceof ListWaypointItem &&
selected[selected.length - 1] instanceof ListWaypointItem
) {
let fileId = selected[0].getFileId();
let file = fileStateCollection.getFile(fileId);
if (file) {
let numberOfWaypoints = file.wpt.length;
let waypointIndex = down
? selected[selected.length - 1].getWaypointIndex()
: selected[0].getWaypointIndex();
if (down && waypointIndex < numberOfWaypoints - 1) {
next = new ListWaypointItem(fileId, waypointIndex + 1);
} else if (!down && waypointIndex > 0) {
next = new ListWaypointItem(fileId, waypointIndex - 1);
}
}
}
if (next && (!get(this._selection).has(next) || !shift)) {
if (shift) {
this.addSelectItem(next);
} else {
this.selectItem(next);
}
}
}
getOrderedSelection(reverse: boolean = false): ListItem[] { getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = []; let selected: ListItem[] = [];
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {

View File

@@ -2,7 +2,7 @@
"metadata": { "metadata": {
"home_title": "edytor online plików GPX", "home_title": "edytor online plików GPX",
"app_title": "Aplikacja", "app_title": "Aplikacja",
"embed_title": "online'owy edytor plików GPX", "embed_title": "Online'owy edytor plików GPX",
"help_title": "pomoc", "help_title": "pomoc",
"404_title": "nie odnaleziono strony", "404_title": "nie odnaleziono strony",
"description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych." "description": "Przeglądaj, edytuj i twórz pliki GPX online z zaawansowanymi możliwościami planowania trasy i narzędziami do przetwarzania plików, pięknymi mapami i szczegółowymi wizualizacjami danych."
@@ -304,7 +304,7 @@
"openTopoMap": "OpenTopoMap", "openTopoMap": "OpenTopoMap",
"openHikingMap": "OpenHikingMap", "openHikingMap": "OpenHikingMap",
"cyclOSM": "CyclOSM", "cyclOSM": "CyclOSM",
"utagawaVTT": "UtagawaMTB", "utagawaVTT": "",
"linz": "LINZ Topo", "linz": "LINZ Topo",
"linzTopo": "LINZ Topo50", "linzTopo": "LINZ Topo50",
"swisstopoRaster": "swisstopo Raster", "swisstopoRaster": "swisstopo Raster",