nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

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:
Msrc/lib/leaflet.control.track-list/track-list.css | 7+++++--
Msrc/lib/leaflet.control.track-list/track-list.js | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/lib/polygon-area/index.js | 34++++++++++++++++++++++++++++++++++
Asrc/lib/polyline-selfintersects/index.js | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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};