mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 18:12:11 +00:00
Compare commits
28 Commits
3a65f8dc16
...
l10n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe6565527 | ||
|
|
69b018022d | ||
|
|
467cb2e589 | ||
|
|
f13d8c1e22 | ||
|
|
e230d55b82 | ||
|
|
46fcdb4bb2 | ||
|
|
429212ef23 | ||
|
|
4ea0ea6a7a | ||
|
|
2e3ce83605 | ||
|
|
fda908dd0d | ||
|
|
cad77e2b10 | ||
|
|
3542a7c24d | ||
|
|
0d6d161e23 | ||
|
|
89a2e0086b | ||
|
|
cd443faf61 | ||
|
|
bfc56b02a8 | ||
|
|
25bafc6bf1 | ||
|
|
6387580626 | ||
|
|
09b8aa65fc | ||
|
|
6c15193f32 | ||
|
|
4442e29b66 | ||
|
|
b6f96d9f4d | ||
|
|
36b66100f9 | ||
|
|
49d8143cc6 | ||
|
|
fc279fecaf | ||
|
|
bd307daa57 | ||
|
|
7a72f44722 | ||
|
|
8e63fc6946 |
242
gpx/src/gpx.ts
242
gpx/src/gpx.ts
@@ -17,6 +17,9 @@ import {
|
|||||||
import { immerable, isDraft, original, freeze } from 'immer';
|
import { immerable, isDraft, original, freeze } from 'immer';
|
||||||
|
|
||||||
function cloneJSON<T>(obj: T): T {
|
function cloneJSON<T>(obj: T): T {
|
||||||
|
if (obj === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (obj === null || typeof obj !== 'object') {
|
if (obj === null || typeof obj !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -818,9 +821,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
|
|
||||||
statistics.local.points = this.trkpt.map((point) => point);
|
statistics.local.points = this.trkpt.map((point) => point);
|
||||||
|
|
||||||
statistics.local.elevation.smoothed = this._computeSmoothedElevation();
|
|
||||||
statistics.local.slope.at = this._computeSlope();
|
|
||||||
|
|
||||||
const points = this.trkpt;
|
const points = this.trkpt;
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
points[i]._data['index'] = i;
|
points[i]._data['index'] = i;
|
||||||
@@ -835,21 +835,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
|
|
||||||
statistics.local.distance.total.push(statistics.global.distance.total);
|
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||||
|
|
||||||
// elevation
|
|
||||||
if (i > 0) {
|
|
||||||
const ele =
|
|
||||||
statistics.local.elevation.smoothed[i] -
|
|
||||||
statistics.local.elevation.smoothed[i - 1];
|
|
||||||
if (ele > 0) {
|
|
||||||
statistics.global.elevation.gain += ele;
|
|
||||||
} else if (ele < 0) {
|
|
||||||
statistics.global.elevation.loss -= ele;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
|
||||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
|
||||||
|
|
||||||
// time
|
// time
|
||||||
if (points[i].time === undefined) {
|
if (points[i].time === undefined) {
|
||||||
statistics.local.time.total.push(0);
|
statistics.local.time.total.push(0);
|
||||||
@@ -960,8 +945,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[statistics.local.slope.segment, statistics.local.slope.length] =
|
this._elevationComputation(statistics);
|
||||||
this._computeSlopeSegments(statistics);
|
|
||||||
|
|
||||||
statistics.global.time.total =
|
statistics.global.time.total =
|
||||||
statistics.global.time.start && statistics.global.time.end
|
statistics.global.time.start && statistics.global.time.end
|
||||||
@@ -977,59 +961,63 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(
|
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
|
||||||
points,
|
|
||||||
200,
|
|
||||||
(accumulated, start, end) =>
|
|
||||||
points[start].time && points[end].time
|
points[start].time && points[end].time
|
||||||
? (3600 * accumulated) /
|
? (3600 *
|
||||||
(points[end].time.getTime() - points[start].time.getTime())
|
(statistics.local.distance.total[end] -
|
||||||
|
statistics.local.distance.total[start])) /
|
||||||
|
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeSmoothedElevation(): number[] {
|
_elevationComputation(statistics: GPXStatistics) {
|
||||||
const points = this.trkpt;
|
|
||||||
|
|
||||||
let smoothed = distanceWindowSmoothing(
|
|
||||||
points,
|
|
||||||
100,
|
|
||||||
(index) => points[index].ele ?? 0,
|
|
||||||
(accumulated, start, end) => accumulated / (end - start + 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (points.length > 0) {
|
|
||||||
smoothed[0] = points[0].ele ?? 0;
|
|
||||||
smoothed[points.length - 1] = points[points.length - 1].ele ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return smoothed;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeSlope(): number[] {
|
|
||||||
const points = this.trkpt;
|
|
||||||
|
|
||||||
return distanceWindowSmoothingWithDistanceAccumulator(
|
|
||||||
points,
|
|
||||||
50,
|
|
||||||
(accumulated, start, end) =>
|
|
||||||
(100 * ((points[end].ele ?? 0) - (points[start].ele ?? 0))) /
|
|
||||||
(accumulated > 0 ? accumulated : 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
|
|
||||||
let simplified = ramerDouglasPeucker(
|
let simplified = ramerDouglasPeucker(
|
||||||
this.trkpt,
|
this.trkpt,
|
||||||
20,
|
20,
|
||||||
getElevationDistanceFunction(statistics)
|
getElevationDistanceFunction(statistics)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
|
let start = simplified[i].point._data.index;
|
||||||
|
let end = simplified[i + 1].point._data.index;
|
||||||
|
|
||||||
|
let cumulEle = 0;
|
||||||
|
let currentStart = start;
|
||||||
|
let currentEnd = start;
|
||||||
|
let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => {
|
||||||
|
for (let i = currentStart; i < s; i++) {
|
||||||
|
cumulEle -= this.trkpt[i].ele ?? 0;
|
||||||
|
}
|
||||||
|
for (let i = currentEnd; i <= e; i++) {
|
||||||
|
cumulEle += this.trkpt[i].ele ?? 0;
|
||||||
|
}
|
||||||
|
currentStart = s;
|
||||||
|
currentEnd = e + 1;
|
||||||
|
return cumulEle / (e - s + 1);
|
||||||
|
});
|
||||||
|
smoothedEle[0] = this.trkpt[start].ele ?? 0;
|
||||||
|
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
|
||||||
|
|
||||||
|
for (let j = start; j < end; j++) {
|
||||||
|
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||||
|
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||||
|
|
||||||
|
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
|
||||||
|
if (ele > 0) {
|
||||||
|
statistics.global.elevation.gain += ele;
|
||||||
|
} else if (ele < 0) {
|
||||||
|
statistics.global.elevation.loss -= ele;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||||
|
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||||
|
|
||||||
let slope = [];
|
let slope = [];
|
||||||
let length = [];
|
let length = [];
|
||||||
|
|
||||||
for (let i = 0; i < simplified.length - 1; i++) {
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
@@ -1043,7 +1031,20 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [slope, length];
|
statistics.local.slope.segment = slope;
|
||||||
|
statistics.local.slope.length = length;
|
||||||
|
statistics.local.slope.at = distanceWindowSmoothing(
|
||||||
|
0,
|
||||||
|
this.trkpt.length,
|
||||||
|
statistics,
|
||||||
|
0.05,
|
||||||
|
(start, end) => {
|
||||||
|
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
||||||
|
const dist =
|
||||||
|
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
|
return dist > 0 ? (0.1 * ele) / dist : 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumberOfTrackPoints(): number {
|
getNumberOfTrackPoints(): number {
|
||||||
@@ -1290,8 +1291,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
lastPoint: TrackPoint | undefined
|
lastPoint: TrackPoint | undefined
|
||||||
) {
|
) {
|
||||||
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||||
let slope = og._computeSlope();
|
let statistics = og._computeStatistics();
|
||||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
let trkpt = withArtificialTimestamps(
|
||||||
|
og.trkpt,
|
||||||
|
totalTime,
|
||||||
|
lastPoint,
|
||||||
|
startTime,
|
||||||
|
statistics.local.slope.at
|
||||||
|
);
|
||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1488,12 +1495,18 @@ export class Waypoint {
|
|||||||
this.attributes = waypoint.attributes;
|
this.attributes = waypoint.attributes;
|
||||||
this.ele = waypoint.ele;
|
this.ele = waypoint.ele;
|
||||||
this.time = waypoint.time;
|
this.time = waypoint.time;
|
||||||
this.name = waypoint.name;
|
this.name = waypoint.name === '' ? undefined : waypoint.name;
|
||||||
this.cmt = waypoint.cmt;
|
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
|
||||||
this.desc = waypoint.desc;
|
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
|
||||||
this.link = waypoint.link;
|
this.link =
|
||||||
this.sym = waypoint.sym;
|
!waypoint.link ||
|
||||||
this.type = waypoint.type;
|
!waypoint.link.attributes ||
|
||||||
|
!waypoint.link.attributes.href ||
|
||||||
|
waypoint.link.attributes.href === ''
|
||||||
|
? undefined
|
||||||
|
: waypoint.link;
|
||||||
|
this.sym = waypoint.sym === '' ? undefined : waypoint.sym;
|
||||||
|
this.type = waypoint.type === '' ? undefined : waypoint.type;
|
||||||
if (waypoint.hasOwnProperty('_data')) {
|
if (waypoint.hasOwnProperty('_data')) {
|
||||||
this._data = waypoint._data;
|
this._data = waypoint._data;
|
||||||
}
|
}
|
||||||
@@ -1647,7 +1660,6 @@ export class GPXStatistics {
|
|||||||
};
|
};
|
||||||
speed: number[];
|
speed: number[];
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: number[];
|
|
||||||
gain: number[];
|
gain: number[];
|
||||||
loss: number[];
|
loss: number[];
|
||||||
};
|
};
|
||||||
@@ -1718,7 +1730,6 @@ export class GPXStatistics {
|
|||||||
},
|
},
|
||||||
speed: [],
|
speed: [],
|
||||||
elevation: {
|
elevation: {
|
||||||
smoothed: [],
|
|
||||||
gain: [],
|
gain: [],
|
||||||
loss: [],
|
loss: [],
|
||||||
},
|
},
|
||||||
@@ -1753,9 +1764,6 @@ export class GPXStatistics {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.local.speed = this.local.speed.concat(other.local.speed);
|
this.local.speed = this.local.speed.concat(other.local.speed);
|
||||||
this.local.elevation.smoothed = this.local.elevation.smoothed.concat(
|
|
||||||
other.local.elevation.smoothed
|
|
||||||
);
|
|
||||||
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
|
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
|
||||||
this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment);
|
this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment);
|
||||||
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
||||||
@@ -1911,11 +1919,15 @@ export function distance(
|
|||||||
const rad = Math.PI / 180;
|
const rad = Math.PI / 180;
|
||||||
const lat1 = coord1.lat * rad;
|
const lat1 = coord1.lat * rad;
|
||||||
const lat2 = coord2.lat * rad;
|
const lat2 = coord2.lat * rad;
|
||||||
|
const dLat = lat2 - lat1;
|
||||||
|
const dLon = (coord2.lon - coord1.lon) * rad;
|
||||||
|
|
||||||
|
// Haversine formula - better numerical stability for small distances
|
||||||
const a =
|
const a =
|
||||||
Math.sin(lat1) * Math.sin(lat2) +
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1)));
|
||||||
return maxMeters;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||||
@@ -1942,57 +1954,59 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function windowSmoothing(
|
||||||
points: TrackPoint[],
|
left: number,
|
||||||
distanceWindow: number,
|
right: number,
|
||||||
accumulate: (index: number) => number,
|
distance: (index1: number, index2: number) => number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number,
|
window: number,
|
||||||
remove?: (index: number) => number
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
let start = 0,
|
let start = left;
|
||||||
end = 0,
|
for (var i = left; i < right; i++) {
|
||||||
accumulated = 0;
|
while (start + 1 < i && distance(start, i) > window) {
|
||||||
for (var i = 0; i < points.length; i++) {
|
|
||||||
while (
|
|
||||||
start + 1 < i &&
|
|
||||||
distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow
|
|
||||||
) {
|
|
||||||
if (remove) {
|
|
||||||
accumulated -= remove(start);
|
|
||||||
} else {
|
|
||||||
accumulated -= accumulate(start);
|
|
||||||
}
|
|
||||||
start++;
|
start++;
|
||||||
}
|
}
|
||||||
while (
|
let end = Math.min(i + 2, right);
|
||||||
end < points.length &&
|
while (end < right && distance(i, end) <= window) {
|
||||||
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
|
|
||||||
) {
|
|
||||||
accumulated += accumulate(end);
|
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
result[i] = compute(accumulated, start, end - 1);
|
result.push(compute(start, end - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothingWithDistanceAccumulator(
|
function distanceWindowSmoothing(
|
||||||
points: TrackPoint[],
|
left: number,
|
||||||
distanceWindow: number,
|
right: number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number
|
statistics: GPXStatistics,
|
||||||
|
window: number,
|
||||||
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
return distanceWindowSmoothing(
|
return windowSmoothing(
|
||||||
points,
|
left,
|
||||||
distanceWindow,
|
right,
|
||||||
(index) =>
|
(index1, index2) =>
|
||||||
index > 0
|
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
||||||
? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
|
window,
|
||||||
: 0,
|
compute
|
||||||
compute,
|
);
|
||||||
(index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
|
}
|
||||||
|
|
||||||
|
function timeWindowSmoothing(
|
||||||
|
points: TrackPoint[],
|
||||||
|
window: number,
|
||||||
|
compute: (start: number, end: number) => number
|
||||||
|
): number[] {
|
||||||
|
return windowSmoothing(
|
||||||
|
0,
|
||||||
|
points.length,
|
||||||
|
(index1, index2) =>
|
||||||
|
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||||
|
window,
|
||||||
|
compute
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Coordinates } from './types';
|
|||||||
|
|
||||||
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
|
||||||
|
|
||||||
export function ramerDouglasPeucker(
|
export function ramerDouglasPeucker(
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
epsilon: number = 50,
|
epsilon: number = 50,
|
||||||
@@ -72,65 +70,45 @@ export function crossarcDistance(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metersPerLatitudeDegree = 111320;
|
||||||
|
|
||||||
|
function getMetersPerLongitudeDegree(latitude: number): number {
|
||||||
|
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
|
||||||
|
}
|
||||||
|
|
||||||
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
||||||
// Calculates the shortest distance in meters
|
// Calculates the perpendicular distance in meters
|
||||||
// between an arc (defined by p1 and p2) and a third point, p3.
|
// between a line segment (defined by p1 and p2) and a third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
// Project p3 onto the line defined by p1-p2
|
||||||
if (diff > Math.PI / 2) {
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
return dis13;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Find the closest point on the segment
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const projX = x1 + t * dx;
|
||||||
|
const projY = y1 + t * dy;
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Return distance from p3 to the projected point
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
||||||
let dis14 =
|
|
||||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
|
||||||
if (dis14 > dis12) {
|
|
||||||
return distance(lat2, lon2, lat3, lon3);
|
|
||||||
} else {
|
|
||||||
return Math.abs(dxt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
|
||||||
// Finds the distance between two lat / lon points.
|
|
||||||
return (
|
|
||||||
Math.acos(
|
|
||||||
Math.sin(latA) * Math.sin(latB) +
|
|
||||||
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
|
||||||
) * earthRadius
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
|
||||||
// Finds the bearing from one lat / lon point to another.
|
|
||||||
return Math.atan2(
|
|
||||||
Math.sin(lonB - lonA) * Math.cos(latB),
|
|
||||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectedPoint(
|
export function projectedPoint(
|
||||||
@@ -146,56 +124,39 @@ export function projectedPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||||
// Calculates the point on the line defined by p1 and p2
|
// Calculates the point on the line segment defined by p1 and p2
|
||||||
// that is closest to the third point, p3.
|
// that is closest to the third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
|
||||||
if (diff > Math.PI / 2) {
|
|
||||||
return coord1;
|
return coord1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Project p3 onto the line defined by p1-p2
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Find the closest point on the segment
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
const projX = x1 + t * dx;
|
||||||
let dis14 =
|
const projY = y1 + t * dy;
|
||||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
|
||||||
if (dis14 > dis12) {
|
|
||||||
return coord2;
|
|
||||||
} else {
|
|
||||||
// Determine the closest point (p4) on the great circle
|
|
||||||
const f = dis14 / earthRadius;
|
|
||||||
const lat4 = Math.asin(
|
|
||||||
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
|
|
||||||
);
|
|
||||||
const lon4 =
|
|
||||||
lon1 +
|
|
||||||
Math.atan2(
|
|
||||||
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
|
|
||||||
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { lat: lat4 / rad, lon: lon4 / rad };
|
// Convert back to degrees
|
||||||
}
|
return {
|
||||||
|
lat: projY / metersPerLatitudeDegree,
|
||||||
|
lon: projX / metersPerLongitudeDegree,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
92
website/package-lock.json
generated
92
website/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
variant="link"
|
variant="link"
|
||||||
class="text-base px-0 has-[>svg]:px-0"
|
class="text-base px-0 has-[>svg]:px-0"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,22 +43,32 @@ 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-${d}`,
|
id: 'distance-markers',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'distance-markers',
|
source: 'distance-markers',
|
||||||
filter:
|
filter: [
|
||||||
d === 5
|
'match',
|
||||||
? [
|
['get', 'level'],
|
||||||
|
100,
|
||||||
|
['>=', ['zoom'], 0],
|
||||||
|
50,
|
||||||
|
['>=', ['zoom'], 7],
|
||||||
|
25,
|
||||||
|
[
|
||||||
'any',
|
'any',
|
||||||
['==', ['get', 'level'], 5],
|
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
||||||
['==', ['get', 'level'], 25],
|
['>=', ['zoom'], 11],
|
||||||
]
|
],
|
||||||
: ['==', ['get', 'level'], d],
|
10,
|
||||||
minzoom: minzoom,
|
['>=', ['zoom'], 10],
|
||||||
maxzoom: maxzoom ?? 24,
|
5,
|
||||||
|
['>=', ['zoom'], 11],
|
||||||
|
1,
|
||||||
|
['>=', ['zoom'], 13],
|
||||||
|
false,
|
||||||
|
],
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'distance'],
|
'text-field': ['get', 'distance'],
|
||||||
'text-size': 14,
|
'text-size': 14,
|
||||||
@@ -78,15 +81,12 @@ export class DistanceMarkers {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
map_.moveLayer(`distance-markers-${d}`);
|
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;
|
||||||
|
|||||||
@@ -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"')
|
${
|
||||||
|
layerColor
|
||||||
|
? Square.replace('width="24"', 'width="12"')
|
||||||
.replace('height="24"', 'height="12"')
|
.replace('height="24"', 'height="12"')
|
||||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
.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,11 +371,13 @@ 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());
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{:else if anySelectedLayer(node[id])}
|
{:else if anySelectedLayer(node[id])}
|
||||||
<CollapsibleTreeNode {id}>
|
<CollapsibleTreeNode {id}>
|
||||||
{#snippet trigger()}
|
{#snippet trigger()}
|
||||||
<span>{i18n._(`layers.label.${id}`)}</span>
|
<span>{i18n._(`layers.label.${id}`, id)}</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<div class="ml-2">
|
<div class="ml-2">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { map } from '$lib/components/map/map';
|
|||||||
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
||||||
|
|
||||||
export type CustomOverlay = {
|
export type CustomOverlay = {
|
||||||
|
extensionName: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
tileUrls: string[];
|
tileUrls: string[];
|
||||||
@@ -46,8 +47,16 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addOrUpdateOverlay(overlay: CustomOverlay) {
|
addOrUpdateOverlay(overlay: CustomOverlay) {
|
||||||
if (!overlay.id || !overlay.name || !overlay.tileUrls || overlay.tileUrls.length === 0) {
|
if (
|
||||||
throw new Error('Overlay must have an id, name, and at least one tile URL.');
|
!overlay.extensionName ||
|
||||||
|
!overlay.id ||
|
||||||
|
!overlay.name ||
|
||||||
|
!overlay.tileUrls ||
|
||||||
|
overlay.tileUrls.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Overlay must have an extensionName, id, name, and at least one tile URL.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
overlay.id = this.getOverlayId(overlay.id);
|
overlay.id = this.getOverlayId(overlay.id);
|
||||||
|
|
||||||
@@ -75,10 +84,17 @@ export class ExtensionAPI {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
overlayTree.overlays.world[overlay.id] = true;
|
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
overlayTree.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
|
||||||
|
|
||||||
selectedOverlayTree.update((selected) => {
|
selectedOverlayTree.update((selected) => {
|
||||||
selected.overlays.world[overlay.id] = true;
|
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
selected.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
selected.overlays[overlay.extensionName][overlay.id] = true;
|
||||||
return selected;
|
return selected;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +110,10 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentOverlays.update((current) => {
|
currentOverlays.update((current) => {
|
||||||
current.overlays.world[overlay.id] = show;
|
if (!current.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
current.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
current.overlays[overlay.extensionName][overlay.id] = show;
|
||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -133,6 +152,29 @@ export class ExtensionAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOverlaysOrder(ids: string[]) {
|
||||||
|
ids = ids.map((id) => this.getOverlayId(id));
|
||||||
|
selectedOverlayTree.update((selected) => {
|
||||||
|
let isSelected: Record<string, boolean> = {};
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const overlay = get(this._overlays).get(id);
|
||||||
|
if (
|
||||||
|
overlay &&
|
||||||
|
selected.overlays.hasOwnProperty(overlay.extensionName) &&
|
||||||
|
selected.overlays[overlay.extensionName].hasOwnProperty(id)
|
||||||
|
) {
|
||||||
|
isSelected[id] = selected.overlays[overlay.extensionName][id];
|
||||||
|
delete selected.overlays[overlay.extensionName][id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.entries(isSelected).forEach(([id, value]) => {
|
||||||
|
const overlay = get(this._overlays).get(id)!;
|
||||||
|
selected.overlays[overlay.extensionName][id] = value;
|
||||||
|
});
|
||||||
|
return selected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
||||||
return (id: string) => $overlays.has(id);
|
return (id: string) => $overlays.has(id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class MapboxGLMap {
|
|||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||||
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
|
sprite: 'mapbox://sprites/mapbox/outdoors-v12',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
let endTime: string | undefined = $state(undefined);
|
let endTime: string | undefined = $state(undefined);
|
||||||
let movingTime: number | undefined = $state(undefined);
|
let movingTime: number | undefined = $state(undefined);
|
||||||
let speed: number | undefined = $state(undefined);
|
let speed: number | undefined = $state(undefined);
|
||||||
let artificial = $state(false);
|
let artificial = $state(true);
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
fileActionManager.applyToFile(fileId, (file) => {
|
fileActionManager.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!
|
movingTime!
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce.svelte';
|
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte';
|
||||||
|
|
||||||
let props: { class?: string } = $props();
|
let props: { class?: string } = $props();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/fi
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export const minTolerance = 0.1;
|
export const minTolerance = 0.1;
|
||||||
|
|
||||||
@@ -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() {
|
if (this.map.getSource('split-controls')) {
|
||||||
// Show markers only if they are in the current zoom level and bounds
|
this.map.removeSource('split-controls');
|
||||||
this.shownControls.splice(0, this.shownControls.length);
|
}
|
||||||
|
} catch (e) {
|
||||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
fileId,
|
|
||||||
trackIndex,
|
|
||||||
segmentIndex,
|
|
||||||
marker,
|
|
||||||
inZoom: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.getElement().addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
fileActions.split(
|
fileActions.split(
|
||||||
get(splitAs),
|
get(splitAs),
|
||||||
control.fileId,
|
e.features![0].properties!.fileId,
|
||||||
control.trackIndex,
|
e.features![0].properties!.trackIndex,
|
||||||
control.segmentIndex,
|
e.features![0].properties!.segmentIndex,
|
||||||
control.point.getCoordinates(),
|
{ lon: coordinates[0], lat: coordinates[1] },
|
||||||
control.point._data.index
|
e.features![0].properties!.pointIndex
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,14 +95,14 @@
|
|||||||
desc: description.length > 0 ? description : undefined,
|
desc: description.length > 0 ? description : undefined,
|
||||||
cmt: description.length > 0 ? description : undefined,
|
cmt: description.length > 0 ? description : undefined,
|
||||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||||
sym: sym,
|
sym: sym.length > 0 ? sym : undefined,
|
||||||
},
|
},
|
||||||
selectedWaypoint.wpt && selectedWaypoint.fileId
|
selectedWaypoint.wpt && selectedWaypoint.fileId
|
||||||
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
||||||
: 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>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Locale {
|
|||||||
private _isLoadingInitial = $state(true);
|
private _isLoadingInitial = $state(true);
|
||||||
private _isLoading = $state(true);
|
private _isLoading = $state(true);
|
||||||
private dictionary: Dictionary = $state({});
|
private dictionary: Dictionary = $state({});
|
||||||
private _t = $derived((key: string) => {
|
private _t = $derived((key: string, fallback?: string) => {
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let value: string | Dictionary = this.dictionary;
|
let value: string | Dictionary = this.dictionary;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class Locale {
|
|||||||
if (value && typeof value === 'object' && k in value) {
|
if (value && typeof value === 'object' && k in value) {
|
||||||
value = value[k];
|
value = value[k];
|
||||||
} else {
|
} else {
|
||||||
return key;
|
return fallback || key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"undo": "撤销",
|
"undo": "撤销",
|
||||||
"redo": "恢复",
|
"redo": "恢复",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete_all": "Delete all",
|
"delete_all": "",
|
||||||
"select_all": "全选",
|
"select_all": "全选",
|
||||||
"view": "显示",
|
"view": "显示",
|
||||||
"elevation_profile": "海拔剖面图",
|
"elevation_profile": "海拔剖面图",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
class="text-base w-1/4 min-w-fit rounded-full"
|
class="text-base w-1/4 min-w-fit rounded-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
{i18n._('metadata.description')}
|
{i18n._('metadata.description')}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row justify-center gap-3">
|
<div class="w-full flex flex-row justify-center gap-3">
|
||||||
<Button href={getURLForLanguage(i18n.lang, '/app')} class="w-1/3 min-w-fit">
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
class="w-1/3 min-w-fit"
|
||||||
|
>
|
||||||
<Map size="18" />
|
<Map size="18" />
|
||||||
{i18n._('homepage.app')}
|
{i18n._('homepage.app')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh">
|
<div class="fixed flex flex-row w-dvw h-dvh">
|
||||||
<div class="flex flex-col grow h-full min-w-0">
|
<div class="flex flex-col grow h-full min-w-0">
|
||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Menu />
|
<Menu />
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ const config = {
|
|||||||
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
supports: {
|
|
||||||
dvh: 'height: 100dvh',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plugins: [tailwindcssAnimate],
|
plugins: [tailwindcssAnimate],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user