Refactor map adapters for improved marker and routing management

- Remove deliveryCheckMarker in favor of ID-tracked markers across all adapters
- Add polygon tracking by zone ID in LeafletAdapter for better zone lifecycle management
- Refactor LibreAdapter routing to use MapLibre GL Directions with proper cleanup
- Improve marker recreation logic and click handler persistence in LibreAdapter
- Update updateMarkerPopup interface to accept HTMLElement for type safety
- Add routing profile support (driving/walking/cycling) to RouteOptions
- Enhance destroy methods to properly clean up all resources

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: David Nguyen <david.nguyen@goutezplanb.com>
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Nguyen 2025-11-03 17:21:51 -05:00
parent 30cc53f9ad
commit 5205ebff6c
Signed by: david.nguyen
GPG Key ID: D5FB5A5715829326
5 changed files with 524 additions and 538 deletions

View File

@ -21,6 +21,7 @@
"@angular/router": "^19.2.0",
"@angular/ssr": "^19.2.15",
"@maplibre/maplibre-gl-directions": "^0.8.0",
"@svrnty/ngx-open-map-wrapper": "file:../ngx-open-map-wrapper/dist/ngx-open-map-wrapper",
"express": "^4.18.2",
"leaflet": "^2.0.0-alpha.1",
"leaflet-routing-machine": "^3.2.12",

View File

@ -18,10 +18,10 @@ import {
export class LeafletAdapter implements IMapAdapter<Marker> {
private map!: Map;
private deliveryCheckMarker?: Marker;
private markers: Record<string, Marker> = {};
private popup?: Popup;
private routes: Record<string, any> = {};
private polygons: Record<number, L.Polygon> = {};
init(container: HTMLElement, options: MapOptions): void {
this.map = new Map(container).setView(options.center, options.zoom);
@ -40,24 +40,22 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
}
addMarker(latLng: LatLng, options?: { id?: string; color?: string ; icon? : DivIcon }) {
if (!options?.id) {
console.warn('addMarker called without id, marker will not be tracked');
return;
}
const markerOptions: L.MarkerOptions = {};
if (options?.icon) {
markerOptions.icon = options.icon;
}
if (options?.id) {
if (this.markers[options.id]) {
this.map.removeLayer(this.markers[options.id]);
}
const marker = new Marker(latLng, markerOptions);
marker.addTo(this.map);
this.markers[options.id] = marker;
} else {
if (this.deliveryCheckMarker) {
this.map.removeLayer(this.deliveryCheckMarker);
}
this.deliveryCheckMarker = new Marker(latLng, markerOptions).addTo(this.map);
if (this.markers[options.id]) {
this.map.removeLayer(this.markers[options.id]);
}
const marker = new Marker(latLng, markerOptions);
marker.addTo(this.map);
this.markers[options.id] = marker;
}
getMarker(id: string): Marker | undefined {
return this.markers[id];
@ -68,16 +66,14 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
}
removeMarker(id?: string): void {
if (id) {
if (this.markers[id]) {
this.map.removeLayer(this.markers[id]);
delete this.markers[id];
}
} else {
if (this.deliveryCheckMarker) {
this.map.removeLayer(this.deliveryCheckMarker);
this.deliveryCheckMarker = undefined;
}
if (!id) {
console.warn('removeMarker called without id');
return;
}
if (this.markers[id]) {
this.map.removeLayer(this.markers[id]);
delete this.markers[id];
}
}
@ -92,9 +88,12 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
}
}
updateMarkerPopup(id: string, popup: any): void {
updateMarkerPopup(id: string, content: HTMLElement): void {
const marker = this.markers[id];
if (marker) {
const popup = L.popup({
offset: [-5, -30],
}).setContent(content);
marker.bindPopup(popup);
}
}
@ -113,6 +112,11 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
{
for(let zone of zones)
{
// Remove old polygon if it exists
if (this.polygons[zone.id]) {
this.map.removeLayer(this.polygons[zone.id]);
}
const latlngs = zone.polygon.map((geoPoint) => {
return [geoPoint.x, geoPoint.y] as LatLng;
});
@ -124,8 +128,11 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
if (zone.opacity)
opacity = zone.opacity;
polygon(latlngs, { color, fillOpacity: opacity })
const poly = polygon(latlngs, { color, fillOpacity: opacity })
.addTo(this.map);
// Store polygon reference for later removal/update
this.polygons[zone.id] = poly;
}
}
openZonePopup(zone: Zone) : void
@ -189,7 +196,6 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
};
if (options?.serviceUrl) {
console.log('serviceUrl', options);
routeOptions.router = L.Routing.osrmv1({
serviceUrl: options.serviceUrl
});
@ -222,6 +228,8 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
}
destroy(): void {
this.removeAllRoutes();
// Clean up polygons
Object.values(this.polygons).forEach(poly => this.map.removeLayer(poly));
this.map.remove();
}
}

View File

@ -3,11 +3,10 @@ import {getLngLat, IMapAdapter, LatLng, MapOptions, Zone, RouteOptions} from './
import MapLibreGlDirections from "@maplibre/maplibre-gl-directions";
export class LibreAdapter implements IMapAdapter<Marker> {
private map!: Map;
private deliveryCheckMarker?: Marker;
private markers: Record<string, Marker> = {};
private markerClickHandlers: Record<string, (e: any) => void> = {};
private popup?: Popup;
private routes: Record<string, any> = {};
private routeLayers: Record<string, [string, string]> = {};
private directions: Record<string, MapLibreGlDirections> = {};
init(container: HTMLElement, options: MapOptions): void {
this.map = new Map({
container,
@ -28,6 +27,11 @@ export class LibreAdapter implements IMapAdapter<Marker> {
}
addMarker(latLng: LatLng, options?: { id?: string; color?: string; icon?: HTMLElement }) {
if (!options?.id) {
console.warn('addMarker called without id, marker will not be tracked');
return;
}
const coords = getLngLat(latLng);
const markerOptions: any = {};
@ -39,23 +43,14 @@ export class LibreAdapter implements IMapAdapter<Marker> {
markerOptions.color = 'red';
}
if (options?.id) {
if (this.markers[options.id]) {
this.markers[options.id].remove();
}
const marker = new Marker(markerOptions)
.setLngLat(coords)
.addTo(this.map);
this.markers[options.id] = marker;
} else {
if (this.deliveryCheckMarker) {
this.deliveryCheckMarker.remove();
}
this.deliveryCheckMarker = new Marker(markerOptions)
.setLngLat(coords)
.addTo(this.map);
if (this.markers[options.id]) {
this.markers[options.id].remove();
}
const marker = new Marker(markerOptions)
.setLngLat(coords)
.addTo(this.map);
this.markers[options.id] = marker;
}
getMarker(id: string): Marker | undefined {
@ -67,19 +62,16 @@ export class LibreAdapter implements IMapAdapter<Marker> {
}
removeMarker(id?: string): void {
if (id) {
if (this.markers[id]) {
this.markers[id].remove();
delete this.markers[id];
}
if (!id) {
console.warn('removeMarker called without id');
return;
}
else
{
if (this.deliveryCheckMarker)
{
this.deliveryCheckMarker.remove();
this.deliveryCheckMarker = undefined;
}
if (this.markers[id]) {
this.markers[id].remove();
delete this.markers[id];
// Clean up stored click handler
delete this.markerClickHandlers[id];
}
}
@ -90,13 +82,19 @@ export class LibreAdapter implements IMapAdapter<Marker> {
updateMarkerIcon(id: string, icon: HTMLElement): void {
const marker = this.markers[id];
if (marker) {
marker.getElement().replaceWith(icon);
this.markers[id] = this.recreateMarker(id, marker, icon);
}
}
updateMarkerPopup(id: string, popup: Popup): void {
updateMarkerPopup(id: string, content: HTMLElement): void {
const marker = this.markers[id];
if (marker) {
// Create MapLibre popup with the HTML content
const popup = new Popup({
offset: [0, -30],
maxWidth: '400px',
className: 'delivery-popup'
}).setDOMContent(content);
marker.setPopup(popup);
}
}
@ -104,6 +102,8 @@ export class LibreAdapter implements IMapAdapter<Marker> {
addMarkerClickHandler(id: string, handler: (e: any) => void): void {
const marker = this.markers[id];
if (marker) {
// Store the handler so we can re-apply it when re-adding markers
this.markerClickHandlers[id] = handler;
marker.getElement().addEventListener('click', handler);
}
}
@ -206,51 +206,102 @@ export class LibreAdapter implements IMapAdapter<Marker> {
addRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
this.removeRoute(id);
// Convert waypoints to GeoJSON LineString format [lng, lat]
const directionsConfig: any = {
sourceName: `maplibre-gl-directions-${id}`, // Unique source name per route
requestOptions: {
overview: 'full',
}
};
if (options?.serviceUrl) {
let apiUrl = options.serviceUrl;
apiUrl = apiUrl.replace(/\/$/, '');
if (!apiUrl.endsWith('/route/v1')) {
apiUrl = `${apiUrl}/route/v1`;
}
directionsConfig.api = apiUrl;
directionsConfig.profile = options.profile || 'driving';
}
const directions = new MapLibreGlDirections(this.map, directionsConfig);
directions.interactive = false;
const coordinates = waypoints.map(wp => getLngLat(wp));
directions.setWaypoints(coordinates);
this.directions[id] = directions;
const sourceId = `route-${id}`;
const layerId = `route-layer-${id}`;
// Add the route as a GeoJSON source
this.map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: coordinates
}
}
});
// Add the route layer
this.map.addLayer({
id: layerId,
type: 'line',
source: sourceId,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': options?.lineStyle?.color || '#3388ff',
'line-width': options?.lineStyle?.weight || 4,
'line-opacity': options?.lineStyle?.opacity ?? 1
}
});
this.routeLayers[id] = [sourceId, layerId];
// Optionally add markers for waypoints
if (options?.showMarkers) {
waypoints.forEach((wp, index) => {
const markerId = `${id}-waypoint-${index}`;
this.addMarker(wp, { id: markerId, color: options.lineStyle?.color || '#3388ff' });
this.routes[markerId] = true;
if (options?.lineStyle) {
directions.on('fetchroutesend', () => {
this.applyRouteLineStyle(directionsConfig.sourceName, options.lineStyle);
});
}
this.readdAllMarkers();
}
private recreateMarker(id: string, marker: Marker, element?: HTMLElement): Marker {
const lngLat = marker.getLngLat();
const markerElement = element || marker.getElement();
const popup = marker.getPopup();
// Remove old marker
marker.remove();
// Create new marker with updated element
const newMarker = new Marker({ element: markerElement })
.setLngLat(lngLat)
.addTo(this.map);
// Restore popup if exists
if (popup) {
newMarker.setPopup(popup);
}
// Restore click handler if exists
if (this.markerClickHandlers[id]) {
newMarker.getElement().addEventListener('click', this.markerClickHandlers[id]);
}
return newMarker;
}
private applyRouteLineStyle(sourceName: string, lineStyle?: any): void {
if (!lineStyle) return;
const routeLineLayer = `${sourceName}-routeline`;
const routeLineCasingLayer = `${sourceName}-routeline-casing`;
if (this.map.getLayer(routeLineLayer)) {
if (lineStyle.color) {
this.map.setPaintProperty(routeLineLayer, 'line-color', lineStyle.color);
}
if (lineStyle.opacity !== undefined) {
this.map.setPaintProperty(routeLineLayer, 'line-opacity', lineStyle.opacity);
}
if (lineStyle.weight !== undefined) {
this.map.setPaintProperty(routeLineLayer, 'line-width', lineStyle.weight);
}
}
if (this.map.getLayer(routeLineCasingLayer)) {
if (lineStyle.color) {
this.map.setPaintProperty(routeLineCasingLayer, 'line-color', lineStyle.color);
}
if (lineStyle.opacity !== undefined) {
this.map.setPaintProperty(routeLineCasingLayer, 'line-opacity', lineStyle.opacity * 0.6);
}
}
}
private readdAllMarkers(): void {
// Re-add all markers to bring them to the front
for (const [id, marker] of Object.entries(this.markers)) {
this.markers[id] = this.recreateMarker(id, marker);
}
}
updateRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
@ -258,31 +309,18 @@ export class LibreAdapter implements IMapAdapter<Marker> {
}
removeRoute(id: string): void {
if (this.routeLayers[id]) {
const [sourceId, layerId] = this.routeLayers[id];
if (this.map.getLayer(layerId)) {
this.map.removeLayer(layerId);
if (this.directions[id]) {
try {
this.directions[id].destroy();
} catch (e) {
console.warn('Error clearing directions:', e);
}
if (this.map.getSource(sourceId)) {
this.map.removeSource(sourceId);
}
delete this.routeLayers[id];
delete this.directions[id];
}
// Remove waypoint markers if they exist
Object.keys(this.routes).forEach(key => {
if (key.startsWith(`${id}-waypoint-`)) {
this.removeMarker(key);
delete this.routes[key];
}
});
}
removeAllRoutes(): void {
Object.keys(this.routeLayers).forEach(id => this.removeRoute(id));
Object.keys(this.directions).forEach(id => this.removeRoute(id));
}
on(type: string, event: (e: any) => void): void

View File

@ -32,6 +32,7 @@ export interface RouteLineStyle {
export interface RouteOptions {
serviceUrl?: string;
profile?: 'driving' | 'walking' | 'cycling';
lineStyle?: RouteLineStyle;
showMarkers?: boolean;
draggableWaypoints?: boolean;
@ -49,7 +50,7 @@ export interface IMapAdapter<TMarker = any> {
removeMarker(id?: string): void;
hasMarker(id: string): boolean;
updateMarkerIcon(id: string, icon: any): void;
updateMarkerPopup(id: string, popup: any): void;
updateMarkerPopup(id: string, content: HTMLElement): void;
addMarkerClickHandler(id: string, handler: (e: any) => void): void;
// Zone management

772
yarn.lock

File diff suppressed because it is too large Load Diff