Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 94 additions & 26 deletions src/app/modules/map/fb-map.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,21 @@ import {
S57_CLICKABLE_LAYERS,
S57_NAMES
} from './popovers';
import { FreeboardOpenlayersModule } from 'src/app/modules/map/ol';
import {
FreeboardOpenlayersModule,
FreeboardRouteLayerComponent
} from 'src/app/modules/map/ol';
import { CoordsPipe } from 'src/app/lib/pipes';

import { computeDestinationPoint, getGreatCircleBearing } from 'geolib';
import { toLonLat } from 'ol/proj';
import { fromLonLat, toLonLat } from 'ol/proj';
import {
LineString as OlLineString
} from 'ol/geom';
import {
fromLonLatArray,
mapifyCoords
} from './ol/lib/util';
import { Style, Stroke, Fill } from 'ol/style';
import { Collection, Feature } from 'ol';
import { Feature as GeoJsonFeature } from 'geojson';
Expand Down Expand Up @@ -179,6 +189,8 @@ export class FBMapComponent implements OnInit, OnDestroy {

@ViewChild(MatMenuTrigger, { static: true }) contextMenu: MatMenuTrigger;
@ViewChild('olMap', { static: false }) olMap: MapComponent;
@ViewChild(FreeboardRouteLayerComponent)
routeLayer: FreeboardRouteLayerComponent;

scaleUnits = input<string>('');

Expand Down Expand Up @@ -266,6 +278,8 @@ export class FBMapComponent implements OnInit, OnDestroy {
protected course = inject(CourseService);
protected mapInteract = inject(FBMapInteractService);
protected mapService = inject(MapService);
/** Source feature whose MultiLineString geometry is updated live during route editing. */
private routeEditSourceFeature: Feature | null = null;
private settings = inject(SettingsFacade);
private bottomSheet = inject(MatBottomSheet);
private infoPanel = inject(InfoPanelFacade);
Expand Down Expand Up @@ -446,8 +460,10 @@ export class FBMapComponent implements OnInit, OnDestroy {
) {
if (this.overlay().isSelf) {
this.overlay.update((current) => {
const newPos = this.dfeat.self.position;
const wo = Math.round((current.position[0] - newPos[0]) / 360) * 360;
return Object.assign({}, current, {
position: this.dfeat.self.position,
position: wo !== 0 ? [newPos[0] + wo, newPos[1]] : newPos,
vessel: this.dfeat.self
});
});
Expand All @@ -464,28 +480,16 @@ export class FBMapComponent implements OnInit, OnDestroy {
} else {
if (this.overlay().type === 'ais') {
this.overlay.update((current) => {
const newPos = this.dfeat.ais.get(current.id).position;
const wo = Math.round((current.position[0] - newPos[0]) / 360) * 360;
return Object.assign({}, current, {
position: this.dfeat.ais.get(current.id).position,
position: wo !== 0 ? [newPos[0] + wo, newPos[1]] : newPos,
vessel: this.dfeat.ais.get(current.id)
});
});
}
}
}
if (this.app.mapExtent()[0] < 180 && this.app.mapExtent()[2] > 180) {
// if dateline is in view adjust overlay position to stay with vessel

if (
this.overlay().position[0] < 0 &&
this.overlay().position[0] > -180
) {
this.overlay.update((current) => {
return Object.assign({}, current, {
position: [current.position[0] + 360, current.position[1]]
});
});
}
}
}
this.drawVesselLines(this.app.data.vessels.self.positionReceived);
this.rotateMap();
Expand Down Expand Up @@ -846,13 +850,66 @@ export class FBMapComponent implements OnInit, OnDestroy {
if (this.mapInteract.draw.features.getLength() === 0) {
return;
}
this.mapInteract.startModifying(this.overlay());
this.routeEditSourceFeature = null;
if (this.overlay().type === 'route') {
this.mapInteract.measurementCoords = this.skres.fromCache(
const rawCoords = this.skres.fromCache(
'routes',
this.overlay().id
)[1].feature.geometry.coordinates;
// Keep a reference to the rendered MultiLineString feature so its
// geometry can be updated live after each vertex drop.
this.routeEditSourceFeature =
this.mapInteract.draw.features.getArray()[0] as Feature;
// Give OL Modify a scratch LineString with unwrapped (non-split) coords
// so there are no phantom ±180° boundary vertices during editing.
const mc = mapifyCoords([...rawCoords]);
// Shift the scratch geometry to the world copy the user clicked in.
// OL always returns the primary-world Feature regardless of which
// rendered copy was hit, so we identify the correct copy explicitly.
//
// Strategy: the click's raw Mercator x encodes the world copy (thanks to
// the transform() fix in map.component.ts). mapifyCoords() may produce
// a geometry spanning n world widths (e.g. a route crossing the
// antimeridian multiple times). We search all shifts in the range
// [m-n-1 .. m+n+1] where m = clicked world copy and n = route width in
// world copies, and pick the shift that places the nearest vertex closest
// to the click.
const mercCoords = fromLonLatArray(mc) as Coordinate[];
const clickMerc = fromLonLat(this.overlay().position as [number, number]);
const clickMercX = clickMerc[0];
const clickMercY = clickMerc[1];
const worldWidth = 2 * 20037508.3427892;
let minX = mercCoords[0][0], maxX = mercCoords[0][0];
for (const c of mercCoords) {
if (c[0] < minX) minX = c[0];
if (c[0] > maxX) maxX = c[0];
}
const m = Math.round(clickMercX / worldWidth);
const n = Math.ceil((maxX - minX) / worldWidth);
let bestShift = 0, bestDist = Infinity;
for (let k = m - n - 1; k <= m + n + 1; k++) {
const shift = k * worldWidth;
for (const c of mercCoords) {
const dx = c[0] + shift - clickMercX;
const dy = c[1] - clickMercY;
const dist = dx * dx + dy * dy; // squared Euclidean, no sqrt needed
if (dist < bestDist) { bestDist = dist; bestShift = shift; }
}
}
if (bestShift !== 0) {
mercCoords.forEach((c) => (c[0] += bestShift));
}
const scratchGeom = new OlLineString(mercCoords);
const scratchFeat = new Feature({ geometry: scratchGeom });
scratchFeat.setId(this.routeEditSourceFeature.getId());
scratchFeat.set(
'pointMetadata',
this.routeEditSourceFeature.get('pointMetadata')
);
this.mapInteract.draw.features = new Collection([scratchFeat]);
this.mapInteract.measurementCoords = rawCoords;
}
this.mapInteract.startModifying(this.overlay());
if (featureType === 'anchor') {
this.overlay().type = featureType;
this.mapInteract.draw.forSave.id = featureType;
Expand Down Expand Up @@ -891,8 +948,14 @@ export class FBMapComponent implements OnInit, OnDestroy {
}
let pc;
if (fid.split('.')[0] === 'route') {
// c is a flat Coordinate[] from the scratch LineString (EPSG:3857)
pc = this.transformCoordsArray(c);
this.mapInteract.measurementCoords = pc;
// Re-split at antimeridian and update all source segment features so
// the rendered route reflects the edit after each vertex drop.
if (this.routeLayer) {
this.routeLayer.updateRouteSegments(fid.split('.')[1], pc);
}
} else if (fid.split('.')[0] === 'region') {
for (let e = 0; e < c.length; e++) {
if (this.isCoordsArray(c[e])) {
Expand Down Expand Up @@ -1145,6 +1208,11 @@ export class FBMapComponent implements OnInit, OnDestroy {
let item = null;
const t = id.split('.');
let aid: string;
// Shift stored geographic positions to the clicked world copy so the popup
// appears at the same world copy the user clicked, not the primary world.
const worldOffset = coord ? Math.floor((coord[0] + 180) / 360) * 360 : 0;
const wrapPos = (pos: Position): Position =>
worldOffset !== 0 ? [pos[0] + worldOffset, pos[1]] : pos;

switch (t[0]) {
case 's57':
Expand Down Expand Up @@ -1198,7 +1266,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = 'ais';
poData.isSelf = true;
poData.vessel = this.dfeat.self;
poData.position = this.dfeat.self.position;
poData.position = wrapPos(this.dfeat.self.position);
poData.show = true;
break;
case 'ais-vessels':
Expand All @@ -1209,7 +1277,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = 'ais';
poData.id = aid;
poData.vessel = this.dfeat.ais.get(aid);
poData.position = poData.vessel.position;
poData.position = wrapPos(poData.vessel.position);
poData.show = true;
break;
case 'atons':
Expand All @@ -1221,7 +1289,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = 'aton';
poData.id = id;
poData.aton = this.app.data.atons.get(id);
poData.position = poData.aton.position;
poData.position = wrapPos(poData.aton.position);
poData.show = true;
break;
case 'sar':
Expand All @@ -1231,7 +1299,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = 'aton';
poData.id = id;
poData.aton = this.app.data.sar.get(id);
poData.position = poData.aton.position;
poData.position = wrapPos(poData.aton.position);
poData.show = true;
break;
case 'meteo':
Expand All @@ -1241,7 +1309,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = t[0];
poData.id = id;
poData.aton = this.app.data.meteo.get(id);
poData.position = poData.aton.position;
poData.position = wrapPos(poData.aton.position);
poData.show = true;
break;
case 'aircraft':
Expand All @@ -1251,7 +1319,7 @@ export class FBMapComponent implements OnInit, OnDestroy {
poData.type = t[0];
poData.id = id;
poData.aircraft = this.app.data.aircraft.get(id);
poData.position = poData.aircraft.position;
poData.position = wrapPos(poData.aircraft.position);
poData.show = true;
break;
case 'region':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ export class ChartBoundsLayerComponent extends FBFeatureLayerComponent {
[bounds[2], bounds[3]],
[bounds[0], bounds[3]],
[bounds[0], bounds[1]]
],
0
]
);
return [fromLonLatArray(rect)];
}
Expand Down
14 changes: 10 additions & 4 deletions src/app/modules/map/ol/lib/map.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import RenderEvent from 'ol/render/Event';
import { MapService } from './map.service';
import { MapReadyEvent } from './models';
import { AsyncSubject } from 'rxjs';
import { toLonLat, transformExtent } from 'ol/proj';
import { toLonLat, transform, transformExtent } from 'ol/proj';
import { Coordinate } from 'ol/coordinate';
import { FeatureLike } from 'ol/Feature';
import { Extent } from 'ol/extent';
Expand Down Expand Up @@ -262,15 +262,17 @@ export class MapComponent implements OnInit, OnDestroy {

private emitRightClickEvent = (event: MouseEvent) => {
event.preventDefault();
const c = this.map.getEventCoordinate(event);
// Use getEventCoordinateInternal (EPSG:3857) so the pixel lookup is
// correct and the lon/lat preserves the world-copy offset.
const c = this.map.getEventCoordinateInternal(event);
this.mapRightClick.emit({
features: this.map.getFeaturesAtPixel(
this.map.getPixelFromCoordinateInternal(c),
{
hitTolerance: this.hitTolerance
}
),
lonlat: toLonLat(c)
lonlat: transform(c, 'EPSG:3857', 'EPSG:4326')
});
};
private emitSingleClickEvent = (event: MapBrowserEvent<PointerEvent>) => {
Expand All @@ -286,7 +288,11 @@ export class MapComponent implements OnInit, OnDestroy {
features: this.map.getFeaturesAtPixel(event.pixel, {
hitTolerance: this.hitTolerance
}),
lonlat: toLonLat(event.coordinate)
// Use raw transform (not toLonLat) so the longitude preserves the
// world-copy offset (e.g. 190° for world +1). This ensures that
// fromLonLat() in overlay.component.ts produces the correct Mercator
// position so OL places the popup in the world copy the user clicked.
lonlat: transform(event.coordinate, 'EPSG:3857', 'EPSG:4326')
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Feature } from 'ol';
import { Style, Stroke } from 'ol/style';
import { MultiLineString } from 'ol/geom';
import { MapComponent } from '../map.component';
import { fromLonLatArray, mapifyCoords } from '../util';
import { fromLonLatArray, splitAtAntimeridian } from '../util';
import { AISBaseLayerComponent } from './ais-base.component';

// ** Signal K AIS targets track **
Expand Down Expand Up @@ -159,16 +159,13 @@ export class AISTargetsTrackLayerComponent extends AISBaseLayerComponent {
}
}

// ** mapify and transform MultiLineString coordinates
// ** split at antimeridian and transform MultiLineString coordinates
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parseCoordinates(trk: Array<any>) {
// ** handle dateline crossing **
const tc = trk.map((mls) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const lines: Array<any> = [];
mls.forEach((line) => lines.push(mapifyCoords(line)));
return lines;
});
const tc = trk.map((mls) =>
mls.flatMap((line) => splitAtAntimeridian(line))
);
return fromLonLatArray(tc);
}
}
Loading