Skip to content

Commit 00466eb

Browse files
committed
added(editor): introduces [readConnectedLinks](https://heremaps.github.io/xyz-maps/docs/classes/core.editablefeatureprovider.html#readconnectedlinks) attribute reader for custom intersection connectivity and extends geometric intersection detection
Signed-off-by: Tim Deubler <tim.deubler@here.com>
1 parent bca6cb9 commit 00466eb

File tree

7 files changed

+130
-63
lines changed

7 files changed

+130
-63
lines changed

packages/core/src/providers/EditableFeatureProvider.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,54 @@ export abstract class EditableFeatureProvider extends FeatureTileProvider {
270270
*/
271271
abstract writeRoutingLink(feature: Feature, position, navlink: Navlink | null);
272272

273+
/**
274+
* Read intersection-related connectivity for a {@link editor.Navlink | Navlink} node.
275+
*
276+
* This method allows an EditableFeatureProvider to override the
277+
* default geometric intersection detection.
278+
*
279+
* It is called for the start or end node of a {@link editor.Navlink | Navlink} to determine
280+
* whether that node forms an intersection and which {@link editor.Navlink | Navlinks} are
281+
* connected to it.
282+
*
283+
* Implement this if intersection information is stored in feature
284+
* properties, derived from external metadata, or otherwise known
285+
* in advance.
286+
*
287+
* Return values:
288+
*
289+
* - `undefined`
290+
* → No custom information is provided.
291+
* The editor falls back to geometric intersection detection.
292+
*
293+
* - `[]` (empty array)
294+
* → Explicitly *no* connected links at this node.
295+
* The editor will NOT perform geometric detection.
296+
*
297+
* - `[ { id, index? }, ... ]`
298+
* → Explicitly defined connected Navlinks, referenced by their
299+
* feature ID.
300+
* If `index` is omitted, the editor automatically determines
301+
* the corresponding node coordinate of the connected link.
302+
*
303+
* @param link - The Navlink feature whose node connectivity is being read.
304+
* @param index - The coordinate index of the node
305+
* (0 → start node, last coordinate → end node).
306+
*
307+
* Note: Regardless of the returned value, geometric requirements for intersection
308+
* (such as spatial proximity and coordinate precision) must still be fulfilled.
309+
* See the {@link EditorOptions.intersectionScale | intersectionScale} property for details.
310+
*
311+
* @returns An array of `{ id: string | number; index?: number }`,
312+
* an empty array to signal "no connection",
313+
* or `undefined` to fall back to default detection.
314+
*/
315+
abstract readConnectedLinks?(
316+
link: Navlink,
317+
index: number
318+
): Array<{ link: string | number; index?: number }> | [] | undefined;
319+
320+
273321
/**
274322
* Attribute writer for storing the EditStates of a Feature.
275323
* The EditStates provide information about whether a feature has been created, modified, removed or split.

packages/core/src/providers/LocalProvider.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface LocalProviderOptions extends EditableFeatureProviderOptions {
5151
*/
5252
storage?: TileStorage;
5353

54-
size?: number
54+
size?: number;
5555
}
5656

5757
/**
@@ -129,7 +129,10 @@ export class LocalProvider extends EditableFeatureProvider {
129129
throw new Error(METHOD_NOT_IMPLEMENTED);
130130
}
131131

132-
writeTurnRestriction(restricted: boolean, turnFrom: { link: Feature; index: number; }, turnTo: { link: Feature; index: number; }) {
132+
writeTurnRestriction(restricted: boolean, turnFrom: { link: Feature; index: number; }, turnTo: {
133+
link: Feature;
134+
index: number;
135+
}) {
133136
throw new Error(METHOD_NOT_IMPLEMENTED);
134137
}
135138

@@ -153,7 +156,10 @@ export class LocalProvider extends EditableFeatureProvider {
153156
throw new Error(METHOD_NOT_IMPLEMENTED);
154157
}
155158

156-
readTurnRestriction(turnFrom: { link: Feature; index: number; }, turnTo: { link: Feature; index: number; }): boolean {
159+
readTurnRestriction(turnFrom: { link: Feature; index: number; }, turnTo: {
160+
link: Feature;
161+
index: number;
162+
}): boolean {
157163
throw new Error(METHOD_NOT_IMPLEMENTED);
158164
}
159165

@@ -170,6 +176,13 @@ export class LocalProvider extends EditableFeatureProvider {
170176
throw new Error('Method not implemented.');
171177
}
172178

179+
readConnectedLinks?(
180+
link: Navlink,
181+
index: number
182+
): Array<{ link: string | number; index?: number }> | [] | undefined {
183+
return undefined;
184+
}
185+
173186
writeEditState(feature, editState: 'created' | 'modified' | 'removed' | 'split') {
174187
}
175188

packages/core/src/providers/RemoteTileProvider/EditableRemoteTileProvider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,13 @@ export abstract class EditableRemoteTileProvider extends EditableFeatureProvider
832832
throw new Error(METHOD_NOT_IMPLEMENTED);
833833
}
834834

835+
readConnectedLinks?(
836+
link: Navlink,
837+
index: number
838+
): Array<{ link: string | number; index?: number }> | [] | undefined {
839+
return undefined;
840+
}
841+
835842
reserveId(createdFeatures, cb: (ids: (string | number)[]) => void) {
836843
let len = createdFeatures.length;
837844
const ids = [];

packages/editor/src/features/link/Navlink.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -262,38 +262,27 @@ export class Navlink extends Feature {
262262

263263
getConnectedLinks(index: number, details: boolean = false) {
264264
const line = this;
265-
const EDITOR = line._e();
266265
const path = line.coord();
267-
const c2 = path[index];
268-
const cLinks = [];
269-
let elPath;
270-
let lastIndex;
271-
const isNode = index == 0 || index == path.length - 1;
272-
273-
const ignoreZ = oTools.ignoreZ(line);
274-
275-
if (isNode /* &&!line.editState('removed')*/) {
276-
for (let feature of EDITOR.objects.getInBBox(line.bbox, line.getProvider())) {
277-
if (feature.id != line.id && feature.class == 'NAVLINK') {
278-
elPath = feature.coord();
279-
lastIndex = elPath.length - 1;
280-
281-
index = oTools.isIntersection(EDITOR, elPath[0], c2, ignoreZ)
282-
? 0
283-
: oTools.isIntersection(EDITOR, elPath[lastIndex], c2, ignoreZ)
284-
? lastIndex
285-
: null;
286-
287-
if (index != null /* && zLevels[shpIndex] == curEl.getZLevels()[index]*/) {
288-
cLinks.push(details ? {
289-
index: index,
290-
link: feature
291-
} : feature);
266+
const node = path[index];
267+
const custom = line.getProvider().readConnectedLinks?.(line, index);
268+
if (custom !== undefined) {
269+
const connected = [];
270+
if (custom === null || custom.length === 0) return connected;
271+
272+
for (const item of custom) {
273+
const connectedLink = line.getProvider().search({id: item.link}) as Navlink;
274+
if (connectedLink) {
275+
const autoIndex = oTools._findIntersectionNodeIndex(connectedLink, node, oTools.ignoreZ(line));
276+
const connectedIndex = item.index ?? autoIndex;
277+
// Ensure the connected link's node still meets geometric intersection requirements (spatial proximity)
278+
if (typeof autoIndex === 'number') {
279+
connected.push(details ? {link: connectedLink, index: connectedIndex} : connectedLink);
292280
}
293281
}
294282
}
283+
return connected;
295284
}
296-
return cLinks;
285+
return oTools._findGeometricIntersectionLinks(line, index, details);
297286
};
298287

299288
/**

packages/editor/src/features/link/NavlinkShape.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ function onMouseMoveShape(ev, dx, dy) {
191191
let curPos = <GeoJSONCoordinate>dragFeatureCoordinate(ev.mapX, ev.mapY, shp, orgCoordinate, EDITOR);
192192
if (ignoreZ) {
193193
// restore initial altitude
194-
curPos[2] = shp.geometry.coordinates[2]||0;
194+
curPos[2] = shp.geometry.coordinates[2] || 0;
195195
}
196196

197197
if (!cfg.editRestrictions(link, EDIT_RESTRICTION.GEOMETRY)) {
@@ -348,7 +348,7 @@ function mouseInHandler() {
348348

349349
prv.cLinks.forEach(({link}) => {
350350
const customStyle = EDITOR.getCustomStyle(link);
351-
let style: StyleGroup & {__default?: StyleGroup};
351+
let style: StyleGroup & { __default?: StyleGroup };
352352

353353
if (customStyle) {
354354
style = JSUtils.extend(true, [], customStyle);
@@ -596,12 +596,10 @@ class NavlinkShape extends Feature {
596596
*/
597597
disconnect() {
598598
const shape = this;
599-
const prv = getPrivate(shape);
600-
const EDITOR = prv.line._e();
599+
const {cLinks, line, index} = getPrivate(shape);
600+
const EDITOR = line._e();
601601

602-
if (shape.isNode() && prv.cLinks.length) {
603-
const line = prv.line;
604-
const index = prv.index;
602+
if (shape.isNode() && cLinks.length) {
605603
const coords = line.coord();
606604
const p1 = coords[index];
607605
const p2 = coords[index + (!index ? 1 : -1)];
@@ -612,7 +610,6 @@ class NavlinkShape extends Feature {
612610
bearing
613611
);
614612

615-
616613
EDITOR.hooks.trigger('Navlink.disconnect', {
617614
link: line,
618615
index: index

packages/editor/src/features/link/NavlinkTools.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import {Feature, GeoJSONBBox as BBox, GeoJSONCoordinate, StyleGroup} from '@here/xyz-maps-core';
2121
import {geotools, JSUtils} from '@here/xyz-maps-common';
22-
import {getPointAtLength, getTotalLength, getPntAt, getSegmentIndex} from '../../geometry';
22+
import {getPointAtLength, getTotalLength, getPntAt, getSegmentIndex, getClosestPntOnLine} from '../../geometry';
2323
import {calcRelPosOfPoiAtLink} from '../../map/GeoMath';
2424
import locTools from '../location/LocationTools';
2525
import {NavlinkShape} from './NavlinkShape';
@@ -231,7 +231,7 @@ function onPointerLeave(ev) {
231231
}
232232

233233

234-
var tools = {
234+
const tools = {
235235

236236
private: getPrivate,
237237

@@ -367,6 +367,38 @@ var tools = {
367367
return !line._e().getStyleProperty(line, 'altitude');
368368
},
369369

370+
_findIntersectionNodeIndex: (link: Navlink, node: GeoJSONCoordinate, ignoreZ?: boolean): number => {
371+
const coordinates = link.geometry.coordinates as GeoJSONCoordinate[];
372+
const lastIndex = coordinates.length - 1;
373+
return tools.isIntersection(link._e(), coordinates[0], node, ignoreZ)
374+
? 0
375+
: tools.isIntersection(link._e(), coordinates[lastIndex], node, ignoreZ)
376+
? lastIndex
377+
: null;
378+
},
379+
_findGeometricIntersectionLinks: (line: Navlink, index: number, details: boolean = true): ({
380+
link: Navlink,
381+
index: number
382+
}[]) => {
383+
const connected = [];
384+
const coordinates = line.geometry.coordinates as GeoJSONCoordinate[];
385+
const isNode = index == 0 || index == coordinates.length - 1;
386+
if (isNode /* &&!line.editState('removed')*/) {
387+
const node = coordinates[index];
388+
for (let feature of line._e().objects.getInBBox(line.bbox, line.getProvider())) {
389+
if (feature.id != line.id && feature.class == 'NAVLINK') {
390+
const connectedNodeIndex = tools._findIntersectionNodeIndex(feature as Navlink, node, tools.ignoreZ(line));
391+
if (connectedNodeIndex != null /* && zLevels[shpIndex] == curEl.getZLevels()[index]*/) {
392+
connected.push(details ? {
393+
index: connectedNodeIndex,
394+
link: feature
395+
} : feature);
396+
}
397+
}
398+
}
399+
}
400+
return connected;
401+
},
370402

371403
//* ****************************************** protected link/shape only *******************************************
372404

packages/editor/src/index.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,8 @@ export {Crossing} from './API/MCrossing';
5252

5353
export {DrawingShape} from './tools/drawingBoards/DrawingShape';
5454

55-
const NAVLINK = 'NAVLINK';
56-
const AREA = 'AREA';
57-
const MARKER = 'MARKER';
58-
const PLACE = 'PLACE';
59-
const ADDRESS = 'ADDRESS';
60-
const LINE = 'LINE';
61-
const objectTypeMapping = {};
6255
let UNDEF;
6356

64-
objectTypeMapping[NAVLINK] = 'Navlink';
65-
objectTypeMapping[AREA] = 'Area';
66-
objectTypeMapping[PLACE] = 'Place';
67-
objectTypeMapping[ADDRESS] = 'Address';
68-
objectTypeMapping[MARKER] = 'Marker';
69-
objectTypeMapping[LINE] = 'Line';
70-
7157

7258
// support for legacy api
7359
export const features = ((() => {
@@ -107,13 +93,11 @@ export const features = ((() => {
10793
}, properties || {});
10894

10995
that.geometry = {
110-
11196
type: (objType == 'PLACE' || objType == 'ADDRESS' || objType == 'MARKER')
11297
? 'Point'
11398
: objType == 'AREA'
11499
? 'MultiPolygon'
115100
: 'LineString',
116-
117101
coordinates: toGeojsonCoordinates(coords)
118102
};
119103
}
@@ -123,13 +107,10 @@ export const features = ((() => {
123107
return Obj;
124108
}
125109

126-
const objects = {};
127-
128-
for (const t in objectTypeMapping) {
129-
objects[objectTypeMapping[t]] = createObjDef(t);
130-
}
131-
132-
return objects;
110+
return Object.fromEntries(
111+
['Navlink', 'Area', 'Place', 'Address', 'Marker', 'Line']
112+
.map((t) => [t, createObjDef(t.toUpperCase())])
113+
);
133114
}))();
134115

135116
export {Editor};

0 commit comments

Comments
 (0)