commit e9f167d473ef91d0a9ab4186b8957965c9d0329d
parent bfeaba93b6a2e05aa00f8875753c7c60c19f679d
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Tue, 29 Jul 2025 13:40:48 +0200
tracks: add segment info to tooltips
Now tooltips for track segments display
- track name (not changed)
- segment ordinal number
- segment length
- area of polygon, surrounded by segment
Diffstat:
4 files changed, 195 insertions(+), 5 deletions(-)
diff --git a/src/lib/leaflet.control.track-list/track-list.css b/src/lib/leaflet.control.track-list/track-list.css
@@ -282,4 +282,8 @@
line-height: 10px;
border-radius: 5px;
color: hsl(100, 0%, 40%);
-}
-\ No newline at end of file
+}
+
+.track-tooltip-area-calc-error {
+ color: #bbbbbb;
+}
diff --git a/src/lib/leaflet.control.track-list/track-list.js b/src/lib/leaflet.control.track-list/track-list.js
@@ -29,6 +29,8 @@ import {splitLinesAt180Meridian} from "./lib/meridian180";
import {ElevationProvider} from '~/lib/elevations';
import {parseNktkSequence} from './lib/parsers/nktk';
import * as coordFormats from '~/lib/leaflet.control.coordinates/formats';
+import {polygonArea} from '~/lib/polygon-area';
+import {polylineHasSelfIntersections} from '~/lib/polyline-selfintersects';
const TRACKLIST_TRACK_COLORS = ['#77f', '#f95', '#0ff', '#f77', '#f7f', '#ee5'];
@@ -500,6 +502,27 @@ L.Control.TrackList = L.Control.extend({
return (x / 1000).toFixed(digits) + ' km';
},
+ formatArea: function(sqMeters) {
+ let value, units;
+ if (sqMeters < 100_000) {
+ value = sqMeters;
+ units = 'm²';
+ } else {
+ value = sqMeters / 1_000_000;
+ units = 'km²';
+ }
+ let options;
+ if (value < 10) {
+ options = {maximumFractionDigits: 2, minimumFractionDigits: 2};
+ } else if (value < 100) {
+ options = {maximumFractionDigits: 1, minimumFractionDigits: 1};
+ } else {
+ options = {maximumSignificantDigits: 3};
+ }
+ const formattedValue = value.toLocaleString('ru-RU', options);
+ return `${formattedValue} ${units}`;
+ },
+
setTrackMeasureTicksVisibility: function(track) {
var visible = track.measureTicksShown(),
lines = this.getTrackPolylines(track);
@@ -921,6 +944,50 @@ L.Control.TrackList = L.Control.extend({
}
},
+ formatSegmentTooltip: function(segment) {
+ const track = segment._parentTrack;
+ const trackSegments = this.getTrackPolylines(track);
+ const trackSegmentsCount = trackSegments.length;
+ const segmentOrdinalNumber = trackSegments.indexOf(segment) + 1;
+
+ // avoid slow calculation of self-intersections due to brute-force algorithm
+ const MAX_POINTS_FOR_INTERSECTIONS_CALCULATION = 1000;
+ // avoid noticeable errors in area calculations due to usage of approximate algorithm
+ const MAX_EXTENT_WIDTH = 10;
+ const MAX_EXTENT_HEIGHT = 5;
+
+ let segmentArea;
+ let points = segment.getLatLngs();
+ if (points.length > 1 && points[0].equals(points.at(-1))) {
+ points = points.slice(0, -1);
+ }
+ if (points.length > MAX_POINTS_FOR_INTERSECTIONS_CALCULATION) {
+ segmentArea = '-- <span class="track-tooltip-area-calc-error">(too many points)</span>';
+ }
+ if (!segmentArea) {
+ const bounds = L.latLngBounds(points);
+ if (
+ bounds.getEast() - bounds.getWest() > MAX_EXTENT_WIDTH ||
+ bounds.getNorth() - bounds.getSouth() > MAX_EXTENT_HEIGHT
+ ) {
+ segmentArea = '-- <span class="track-tooltip-area-calc-error">(too big extent)</span>';
+ }
+ }
+ if (!segmentArea && polylineHasSelfIntersections(points)) {
+ segmentArea = '-- <span class="track-tooltip-area-calc-error">(self-intersection)</span>';
+ }
+ if (!segmentArea) {
+ segmentArea = this.formatArea(polygonArea(points));
+ }
+ return `
+ <b>${track.name()}</b><br>
+ <br>
+ Segment number: ${segmentOrdinalNumber} / ${trackSegmentsCount}<br>
+ Segment length: ${this.formatLength(segment.getLength())}<br>
+ Segment area: ${segmentArea}
+ `;
+ },
+
addTrackSegment: function(track, sourcePoints) {
var polyline = new TrackSegment(sourcePoints || [], {
color: this.colors[track.color()],
@@ -939,6 +1006,10 @@ L.Control.TrackList = L.Control.extend({
polyline.on('editend', () => this.onTrackEditEnd(track));
polyline.on('drawend', this.onTrackSegmentDrawEnd, this);
+ if (!L.Browser.touch) {
+ polyline.bindTooltip(() => this.formatSegmentTooltip(polyline), {sticky: true, delay: 500});
+ }
+
// polyline.on('editingstart', polyline.setMeasureTicksVisible.bind(polyline, false));
// polyline.on('editingend', this.setTrackMeasureTicksVisibility.bind(this, track));
track.feature.addLayer(polyline);
@@ -1275,9 +1346,6 @@ L.Control.TrackList = L.Control.extend({
track.visible.subscribe(this.onTrackVisibilityChanged.bind(this, track));
track.measureTicksShown.subscribe(this.setTrackMeasureTicksVisibility.bind(this, track));
track.color.subscribe(this.onTrackColorChanged.bind(this, track));
- if (!L.Browser.touch) {
- track.feature.bindTooltip(() => track.name(), {sticky: true, delay: 500});
- }
track.hover.subscribe(this.onTrackHoverChanged.bind(this, track));
// this.onTrackColorChanged(track);
diff --git a/src/lib/polygon-area/index.js b/src/lib/polygon-area/index.js
@@ -0,0 +1,34 @@
+const EARTH_RADIUS = 6371008.8;
+const RADIANS_PER_DEGREE = Math.PI / 180;
+/**
+ * Calculate the approximate area of the polygon were it projected onto the earth.
+ *
+ * Reference:
+ * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for Polygons on a Sphere",
+ * JPL Publication 07-03, Jet Propulsion
+ * Laboratory, Pasadena, CA, June 2007 https://trs.jpl.nasa.gov/handle/2014/40409
+ */
+function polygonArea(latlngs) {
+ const latlngsLength = latlngs.length;
+ if (latlngsLength < 3) {
+ return 0;
+ }
+ let acc = 0;
+ for (let i = 0; i < latlngsLength; i++) {
+ let iLow = i - 1;
+ if (iLow === -1) {
+ iLow = latlngsLength - 1;
+ }
+ let iHigh = i + 1;
+ if (iHigh === latlngsLength) {
+ iHigh = 0;
+ }
+ acc +=
+ (latlngs[iHigh].lng - latlngs[iLow].lng) *
+ RADIANS_PER_DEGREE *
+ Math.sin(latlngs[i].lat * RADIANS_PER_DEGREE);
+ }
+ return Math.abs(((EARTH_RADIUS * EARTH_RADIUS) / 2) * acc);
+}
+
+export {polygonArea};
diff --git a/src/lib/polyline-selfintersects/index.js b/src/lib/polyline-selfintersects/index.js
@@ -0,0 +1,85 @@
+// From https://www.geeksforgeeks.org/dsa/check-if-two-given-line-segments-intersect/
+
+/* eslint-disable camelcase */
+
+// function to find orientation of ordered triplet (p, q, r)
+// 0 --> p, q and r are collinear
+// 1 --> Clockwise
+// 2 --> Counterclockwise
+function orientation(p, q, r) {
+ const val = (q.lng - p.lng) * (r.lat - q.lat) - (q.lat - p.lat) * (r.lng - q.lng);
+ // collinear
+ if (val === 0) {
+ return 0;
+ }
+ // clock or counterclock wise
+ // 1 for clockwise, 2 for counterclockwise
+ return val > 0 ? 1 : 2;
+}
+
+// function to check if point q lies on line segment 'pr'
+function onSegment(p, q, r) {
+ return (
+ q.lat <= Math.max(p.lat, r.lat) &&
+ q.lat >= Math.min(p.lat, r.lat) &&
+ q.lng <= Math.max(p.lng, r.lng) &&
+ q.lng >= Math.min(p.lng, r.lng)
+ );
+}
+
+// function to check if two line segments intersect
+function segmentsIntersect(seg1_1, seg1_2, seg2_1, seg2_2) {
+ // find the four orientations needed
+ // for general and special cases
+ const o1 = orientation(seg1_1, seg1_2, seg2_1);
+ const o2 = orientation(seg1_1, seg1_2, seg2_2);
+ const o3 = orientation(seg2_1, seg2_2, seg1_1);
+ const o4 = orientation(seg2_1, seg2_2, seg1_2);
+ // general case
+ if (o1 !== o2 && o3 !== o4) {
+ return true;
+ }
+ // special cases
+ // seg1_1, seg1_2 and seg2_1 are collinear and seg2_1 lies on segment seg1
+ if (o1 === 0 && onSegment(seg1_1, seg2_1, seg1_2)) {
+ return true;
+ }
+ // seg1_1, seg1_2 and seg2_2 are collinear and seg2_2 lies on segment seg1
+ if (o2 === 0 && onSegment(seg1_1, seg2_2, seg1_2)) {
+ return true;
+ }
+ // seg2_1, seg2_2 and seg1_1 are collinear and seg1_1 lies on segment seg2
+ if (o3 === 0 && onSegment(seg2_1, seg1_1, seg2_2)) {
+ return true;
+ }
+ // seg2_1, seg2_2 and seg1_2 are collinear and seg1_2 lies on segment seg2
+ if (o4 === 0 && onSegment(seg2_1, seg1_2, seg2_2)) {
+ return true;
+ }
+ return false;
+}
+
+function polylineHasSelfIntersections(latlngs) {
+ if (latlngs.length < 4) {
+ return false;
+ }
+ for (let i = 0; i <= latlngs.length - 3; i++) {
+ const seg1_1 = latlngs[i];
+ const seg1_2 = latlngs[i + 1];
+
+ for (let j = i + 2; j <= latlngs.length - 1; j++) {
+ if (i === 0 && j === latlngs.length - 1) {
+ continue;
+ }
+ const seg2_1 = latlngs[j];
+ const seg2_2 = latlngs[(j + 1) % latlngs.length];
+
+ if (segmentsIntersect(seg1_1, seg1_2, seg2_1, seg2_2)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+export {polylineHasSelfIntersections};