Skip to content

Commit 24e8df6

Browse files
authored
Merge pull request #304 from tyrossel/densifiers
feat: add Densifiers
2 parents 1bd818d + 1304ea6 commit 24e8df6

File tree

10 files changed

+247
-136
lines changed

10 files changed

+247
-136
lines changed

demos/schm/demo.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import TrackManager from '../../src/interaction/TrackManager.ts';
33
import GraphHopperRouter from '../../src/router/GraphHopperRouter.ts';
44
import GraphhopperSnapper from '../../src/snapper/GraphHopperSnapper.ts';
5+
import UnsnappedDensifier from '../../src/densifier/UnsnappedDensifier.ts';
56
import {ExtractFromSegmentProfiler, FallbackProfiler, SwisstopoProfiler} from '../../src/profiler/index.ts';
67
import {styleFunction} from './style';
78
import {createMap} from './swisstopo';
@@ -43,6 +44,8 @@ async function main() {
4344
]
4445
});
4546

47+
const densifier = new UnsnappedDensifier({ });
48+
4649
/**
4750
* @param {MapBrowserEvent} mapBrowserEvent
4851
* @param {string} pointType
@@ -57,6 +60,7 @@ async function main() {
5760
router: router,
5861
snapper: snapper,
5962
profiler: profiler,
63+
densifier: densifier,
6064
trackLayer: trackLayer,
6165
shadowTrackLayer: shadowTrackLayer,
6266
style: styleFunction,

src/densifier/SnappedDensifier.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type Feature from 'ol/Feature.js';
2+
import type LineString from 'ol/geom/LineString.js';
3+
import {Densifier} from './index';
4+
5+
import {MAX_POINTS_PER_REQUEST} from '../profiler/SwisstopoProfiler';
6+
import {Coordinate, distance} from 'ol/coordinate';
7+
8+
const AT_LEAST_A_POINT_EVERY_N_METERS = 10;
9+
const MAX_POINT_DISTANCE_FOR_A_TRACK = 80;
10+
const EXTRA_DISTANCE = 6; //
11+
12+
type SnappedDensifierOptions = {
13+
/** The wanted distance between two adjacent points */
14+
optimalPointDistance?: number;
15+
16+
/** The greatest distance between two adjacent points */
17+
maxPointDistance?: number;
18+
19+
/**
20+
* The maximal number of points allowed for the new geometry
21+
* If this number is reached, the original coordinates will be kept
22+
*/
23+
maxPoints?: number;
24+
25+
/** no points will be inserted if one exists at that extra distance */
26+
extraDistance?: number;
27+
};
28+
29+
/**
30+
* This densifier will insert points to an geometry to increase the point density according to the parameters.
31+
*
32+
* WARNING:: It is assumed that the map projection is in meters, like EPSG:3857 or EPSG:2056, as the
33+
* euclidian distance is used to compute the new points.
34+
*/
35+
export default class SnappedDensifier implements Densifier {
36+
private optimalPointDistance: number;
37+
private maxPointDistance?: number = MAX_POINTS_PER_REQUEST;
38+
private maxPoints?: number = MAX_POINTS_PER_REQUEST * 2;
39+
private extraDistance?: number = EXTRA_DISTANCE;
40+
41+
constructor(parameters: SnappedDensifierOptions) {
42+
this.optimalPointDistance = parameters.optimalPointDistance ?? AT_LEAST_A_POINT_EVERY_N_METERS;
43+
this.maxPointDistance = parameters.maxPointDistance ?? MAX_POINT_DISTANCE_FOR_A_TRACK;
44+
}
45+
46+
densify(segment: Feature<LineString>): void {
47+
let interval = this.optimalPointDistance;
48+
const geometry = segment.getGeometry();
49+
const coordinates = geometry.getCoordinates();
50+
const numberOfPoints = coordinates.length;
51+
if (numberOfPoints >= this.maxPoints) return;
52+
53+
let retry = false;
54+
do {
55+
try {
56+
const newCoordinates = [];
57+
const optimalPointDistancePlusExtra = this.optimalPointDistance + this.extraDistance;
58+
let previousCoordinate: Coordinate;
59+
let addedCount = 0;
60+
for (const coordinate of coordinates) {
61+
if (addedCount === 0) {
62+
newCoordinates.push(coordinate);
63+
addedCount += 1;
64+
previousCoordinate = coordinate;
65+
continue;
66+
}
67+
const xDiff = coordinate[0] - previousCoordinate[0];
68+
const yDiff = coordinate[1] - previousCoordinate[1];
69+
let dist = distance(coordinate, previousCoordinate);
70+
if (dist > optimalPointDistancePlusExtra) {
71+
const stepVector = [(interval * xDiff) / dist, (interval * yDiff) / dist];
72+
while (dist > optimalPointDistancePlusExtra) {
73+
previousCoordinate = [
74+
previousCoordinate[0] + stepVector[0],
75+
previousCoordinate[1] + stepVector[1],
76+
];
77+
dist -= interval;
78+
newCoordinates.push(previousCoordinate);
79+
addedCount += 1;
80+
if (addedCount > this.maxPoints) throw new Error();
81+
}
82+
}
83+
newCoordinates.push(coordinate);
84+
previousCoordinate = coordinate;
85+
addedCount += 1;
86+
if (addedCount > this.maxPoints) throw new Error();
87+
}
88+
geometry.setCoordinates(newCoordinates);
89+
} catch {
90+
interval *= 2; // Double the interval on error
91+
retry = true; // Set retry flag to true to retry the loop
92+
}
93+
} while (retry && interval <= this.maxPointDistance);
94+
console.error('Failed to insert points, Keeping the original ones');
95+
}
96+
}

src/densifier/UnsnappedDensifier.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type Feature from 'ol/Feature.js';
2+
import type LineString from 'ol/geom/LineString.js';
3+
import {Densifier} from './index';
4+
import {distance} from 'ol/coordinate';
5+
6+
const MAX_POINT_DISTANCE_FOR_A_TRACK = 10;
7+
const DEFAULT_MAX_POINTS = 200;
8+
9+
type UnsnappedDensifierOptions = {
10+
/**
11+
* The distance between two points in the new geometry.
12+
*
13+
*/
14+
distance?: number;
15+
16+
/** The maximal number of points allowed for the new geometry */
17+
maxPoints?: number;
18+
};
19+
20+
/**
21+
* This Densifier only applies on unsnapped segments, and will split it into subsegment by creating
22+
* new points on the line, so that the distance between points is not more that `distance`
23+
* (in meters).
24+
*
25+
* The parameter `maxPoints` overrides `maxPointDistance`, and creates a hard limit on the number of
26+
* points created in the new segment.
27+
*
28+
* If the segment already contains points in between, they are discarded and a new straight segment
29+
* between the start and the end of the line is returned.
30+
*
31+
* WARNING:: It is assumed that the map projection is in meters, like EPSG:3857 or EPSG:2056, as the
32+
* euclidian distance is used to compute the new points.
33+
*/
34+
export default class UnsnappedDensifier implements Densifier {
35+
private distance: number;
36+
private maxPoints: number;
37+
38+
constructor(parameters: UnsnappedDensifierOptions) {
39+
this.distance = parameters.distance || MAX_POINT_DISTANCE_FOR_A_TRACK;
40+
this.maxPoints = parameters.maxPoints || DEFAULT_MAX_POINTS;
41+
}
42+
43+
densify(segment: Feature<LineString>): void {
44+
if (segment.get('snapped')) {
45+
console.log('Segment is snapped, skipped densifier');
46+
return;
47+
}
48+
49+
const geometry = segment.getGeometry();
50+
const coordinates = geometry.getCoordinates();
51+
52+
const start = coordinates[0];
53+
const end = coordinates[coordinates.length - 1];
54+
55+
const segment_distance = distance(start, end);
56+
const xDiff = end[0] - start[0];
57+
const yDiff = end[1] - start[1];
58+
const nSubSegments = Math.ceil(segment_distance / this.distance);
59+
const nPoints = Math.min(this.maxPoints, nSubSegments + 1);
60+
const newCoords = new Array(nPoints);
61+
newCoords[0] = start;
62+
for (let i = 1; i < nPoints - 1; i++) {
63+
const x = start[0] + (xDiff * i) / nPoints;
64+
const y = start[1] + (yDiff * i) / nPoints;
65+
newCoords[i] = [x, y];
66+
}
67+
newCoords[nPoints - 1] = end;
68+
69+
geometry.setCoordinates(newCoords);
70+
}
71+
}

src/densifier/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export {default as UnsnappedDensifier} from './UnsnappedDensifier';
2+
export {default as SnappedDensifier} from './SnappedDensifier';
3+
4+
import type Feature from 'ol/Feature.js';
5+
import type LineString from 'ol/geom/LineString.js';
6+
7+
/**
8+
* A Densifier is an object that can modify the geometry of a segment by modifying its coordinates.
9+
* It takes a segment as input, and modifies it in place.
10+
*
11+
* It is called between the router and the profiler.
12+
*
13+
* The densification process is used:
14+
* - when a segment is added to the line
15+
* - when a segment is modified
16+
*
17+
* However, the densification is not applied when a feature is restored, as no modifications are
18+
* expected.
19+
*/
20+
export interface Densifier {
21+
densify(segment: Feature<LineString>): void;
22+
}

src/interaction/TrackData.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default class TrackData {
158158
}
159159
}
160160

161-
/*
161+
/**
162162
* Add a new control point at the end.
163163
*/
164164
pushControlPoint(point: Feature<Point>): AddedControlPoint {
@@ -185,7 +185,7 @@ export default class TrackData {
185185
throw new Error('Internal error: incorrect length');
186186
}
187187

188-
/*
188+
/**
189189
* Deletes the supplied point and all adjacent segments.
190190
* Creates a new segment if the deleted point had two neighbors.
191191
* Updates first/last subtype if needed.
@@ -269,7 +269,7 @@ export default class TrackData {
269269
this.pois.sort((a, b) => a.get('index') - b.get('index'));
270270
}
271271

272-
/*
272+
/**
273273
* Remove the last control point.
274274
*/
275275
deleteLastControlPoint(): Feature<Point | LineString>[] {
@@ -315,7 +315,7 @@ export default class TrackData {
315315
return this.controlPoints.length > 0 || this.segments.length > 0 || this.pois.length > 0;
316316
}
317317

318-
/*
318+
/**
319319
* Deletes the supplied point.
320320
*/
321321
deletePOI(point: Feature<Point>) {

src/interaction/TrackDensifyer.ts

Lines changed: 0 additions & 70 deletions
This file was deleted.

src/interaction/TrackManager.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {SelectEvent} from 'ol/interaction/Select';
2323
import type {Coordinate} from 'ol/coordinate';
2424
import type {FeatureType} from './TrackData';
2525
import type {Snapper} from 'src/snapper';
26+
import { Densifier } from 'src/densifier';
2627

2728
export type TrackMode = 'edit' | '';
2829
export type TrackSubMode = 'addpoi' | 'editpoi' | '';
@@ -50,7 +51,12 @@ export interface Options {
5051
* The profiler instance to add 3d coordinates to segments.
5152
*/
5253
profiler: Profiler
53-
style: StyleLike | FlatStyleLike
54+
/**
55+
* The densifier to use to modify the line geometries
56+
* If not provided, the track will not be densified.
57+
*/
58+
densifier?: Densifier;
59+
style: StyleLike | FlatStyleLike;
5460
/**
5561
* Condition to remove a point (control point or POI). Default is click.
5662
*/
@@ -97,6 +103,7 @@ export default class TrackManager<POIMeta> {
97103
return this.router_;
98104
}
99105
private snapper_: Snapper;
106+
private densifier_: Densifier | undefined;
100107
get snapper(): Snapper {
101108
return this.snapper_;
102109
}
@@ -119,8 +126,10 @@ export default class TrackManager<POIMeta> {
119126
this.router_ = options.router;
120127
this.snapper_ = options.snapper;
121128
this.profiler_ = options.profiler;
129+
this.densifier_ = options.densifier;
122130
this.updater_ = new TrackUpdater({
123131
profiler: this.profiler_,
132+
densifier: this.densifier_,
124133
router: this.router_,
125134
trackData: this.trackData_
126135
});
@@ -163,6 +172,8 @@ export default class TrackManager<POIMeta> {
163172
this.source_.addFeature(segment);
164173
await this.router_.snapSegment(segment, pointFrom, pointTo);
165174
this.updater_.equalizeCoordinates(pointFrom);
175+
176+
if (this.densifier_) this.densifier_.densify(segment);
166177
await this.profiler_.computeProfile(segment);
167178
// FIXME: setZ ?
168179
this.onTrackChanged_();
@@ -409,7 +420,9 @@ export default class TrackManager<POIMeta> {
409420
// should parse features first, compute profile, and then replace the trackdata and add history
410421
const parsedFeatures = this.trackData_.parseFeatures(features);
411422
this.source_.addFeatures(features);
412-
const profileRequests = parsedFeatures.segments.map(segment => this.profiler_.computeProfile(segment));
423+
const profileRequests = parsedFeatures.segments.map((segment) =>
424+
this.profiler_.computeProfile(segment)
425+
);
413426
await Promise.all(profileRequests);
414427
this.trackData_.restoreParsedFeatures(parsedFeatures);
415428
}
@@ -601,4 +614,4 @@ export default class TrackManager<POIMeta> {
601614
this.source_.changed();
602615
this.shadowTrackLayer_.getSource().changed();
603616
}
604-
}
617+
}

0 commit comments

Comments
 (0)