Make IMapAdapter generic and add zone/marker management

This update adds support for adapter-specific marker types and implements comprehensive zone and marker management functionality:

- Make IMapAdapter generic with TMarker type parameter to support different marker implementations
- Fix Leaflet popup implementation to use Popup class instead of invisible markers
- Add marker tracking with getMarker() and getAllMarkers() methods
- Add zone management with addZone(), updateZone(), openZonePopup(), and closePopup()
- Implement marker addition/removal with ID-based tracking
- Update MapFacade to handle generic marker types
- Bump version to 0.2.2

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: davidnguyen <david.nguyen@goutezplanb.com>
This commit is contained in:
David Nguyen 2025-10-27 21:37:59 -04:00
parent 00cc9eab09
commit 46e6f7f44a
Signed by: david.nguyen
GPG Key ID: D5FB5A5715829326
6 changed files with 2058 additions and 2014 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@svrnty/ngx-open-map-wrapper",
"version": "0.2.1",
"version": "0.2.2",
"keywords": [
"maplibre",
"leaflet",

View File

@ -2,16 +2,22 @@ import {
Map,
TileLayer,
Marker,
Popup,
polygon
} from 'leaflet';
import {
getLngLat,
IMapAdapter,
LatLng,
MapOptions,
MapOptions, Zone,
} from './map-adapter.interface';
export class LeafletAdapter implements IMapAdapter {
export class LeafletAdapter implements IMapAdapter<Marker> {
private map!: Map;
private deliveryCheckMarker?: Marker;
private markers: Record<string, Marker> = {};
private popup?: Popup;
init(container: HTMLElement, options: MapOptions): void {
this.map = new Map(container).setView(options.center, options.zoom);
@ -29,9 +35,101 @@ export class LeafletAdapter implements IMapAdapter {
this.map.setZoom(zoom);
}
addMarker(latLng: LatLng, options?: { color?: string }): void {
const marker = new Marker(latLng);
marker.addTo(this.map);
addMarker(latLng: LatLng, options: { id?: string; color?: string }) {
if (options.id) {
if (this.markers[options.id]) {
this.map.removeLayer(this.markers[options.id]);
}
const marker = new Marker(latLng)
marker.addTo(this.map);
this.markers[options.id] = marker;
} else {
if (this.deliveryCheckMarker) {
this.map.removeLayer(this.deliveryCheckMarker);
}
this.deliveryCheckMarker = new Marker(latLng).addTo(this.map);
}
}
getMarker(id: string): Marker | undefined {
return this.markers[id];
}
getAllMarkers(): Record<string, Marker> {
return this.markers;
}
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;
}
}
}
addZone(zones: Zone[]): void
{
this.updateZone(zones);
}
updateZone(zones: Zone[]): void
{
for(let zone of zones)
{
const latlngs = zone.polygon.map((geoPoint) => {
return [geoPoint.x, geoPoint.y] as LatLng;
});
let color = "#ff0000"
let opacity = 0.4;
if(zone.color)
color = zone.color;
if (zone.opacity)
opacity = zone.opacity;
polygon(latlngs, { color, fillOpacity: opacity })
.addTo(this.map);
}
}
openZonePopup(zone: Zone) : void
{
let sumLat = 0;
let sumLng = 0;
zone.polygon.forEach(point => {
sumLat += point.x;
sumLng += point.y;
});
const centerLat = sumLat / zone.polygon.length;
const centerLng = sumLng / zone.polygon.length;
if (this.popup) {
this.map.closePopup(this.popup);
}
this.popup = new Popup()
.setLatLng([centerLat, centerLng])
.setContent(`
<div >
${zone.name ? `<p><strong>${zone.name}</strong></p>` : ''}
${zone.shippingFee ? `<p>Shipping: ${zone.shippingFee}</p>` : ''}
${zone.deliverySchedule ? `<p>Delivery: ${zone.deliverySchedule}</p>` : ''}
</div>`)
.openOn(this.map);
}
closePopup(): void
{
if (this.popup) {
this.map.closePopup(this.popup);
this.popup = undefined;
}
}
on(type: string, event: (e: any) => void): void
{
this.map.on(type, event);
}
destroy(): void {

View File

@ -1,9 +1,11 @@
import { Map, Marker, NavigationControl } from 'maplibre-gl';
import {getLngLat, IMapAdapter, LatLng, MapOptions} from './map-adapter.interface';
import { Map, Marker, NavigationControl, GeoJSONSource, Popup } from 'maplibre-gl';
import {getLngLat, IMapAdapter, LatLng, MapOptions, Zone} from './map-adapter.interface';
export class LibreAdapter implements IMapAdapter {
export class LibreAdapter implements IMapAdapter<Marker> {
private map!: Map;
private deliveryCheckMarker?: Marker;
private markers: Record<string, Marker> = {};
private popup?: Popup;
init(container: HTMLElement, options: MapOptions): void {
this.map = new Map({
container,
@ -23,12 +25,150 @@ export class LibreAdapter implements IMapAdapter {
this.map.setZoom(zoom);
}
addMarker(latLng: LatLng, options?: { color?: string }): void {
new Marker({ color: options?.color || 'red' })
.setLngLat(getLngLat(latLng))
.addTo(this.map);
addMarker(latLng: LatLng, options: { id?: string; color?: string }) {
const coords = getLngLat(latLng);
if (options.id) {
if (this.markers[options.id]) {
this.markers[options.id].remove();
}
const marker = new Marker({ color: options.color || 'red' })
.setLngLat(coords)
.addTo(this.map);
this.markers[options.id] = marker;
} else {
if (this.deliveryCheckMarker) {
this.deliveryCheckMarker.remove();
}
this.deliveryCheckMarker = new Marker({ color: options?.color || 'red' })
.setLngLat(coords)
.addTo(this.map);
}
}
getMarker(id: string): Marker | undefined {
return this.markers[id];
}
getAllMarkers(): Record<string, Marker> {
return this.markers;
}
removeMarker(id?: string): void {
if (id) {
if (this.markers[id]) {
this.markers[id].remove();
delete this.markers[id];
}
}
else
{
if (this.deliveryCheckMarker)
{
this.deliveryCheckMarker.remove();
this.deliveryCheckMarker = undefined;
}
}
}
addZone(zones: Zone[]): void
{
this.updateZone(zones);
}
updateZone(zones: Zone[]): void
{ const features = zones.map((zone) => {
const coords = zone.polygon.map((pt) => [pt.y, pt.x]);
if (coords[0][0] !== coords[coords.length - 1][0] || coords[0][1] !== coords[coords.length - 1][1]) {
coords.push(coords[0]);
}
return {
type: 'Feature' as const,
properties: {
id: zone.id,
name: zone.name,
color: zone.color ? zone.color : '#ff0000',
opacity: zone.opacity ? zone.opacity : 0.4,
},
geometry: {
type: 'Polygon' as const,
coordinates: [coords]
},
};
});
const geojson = {
type: 'FeatureCollection' as const,
features,
};
if (this.map.getSource("zones")) {
(this.map.getSource("zones") as GeoJSONSource).setData(geojson);
}
else {
this.map.addSource("zones", {
type: "geojson",
data: geojson,
});
this.map.addLayer({
id: "zones-fill",
type: "fill",
source: "zones",
paint: {
"fill-color": ["get", "color"],
"fill-opacity": ["get", "opacity"],
},
});
this.map.addLayer({
id: "zones-outline",
type: "line",
source: "zones",
paint: {
"line-color": ["get", "color"],
"line-width": 2,
},
});
}
}
openZonePopup(zone: Zone) : void
{
let sumLng = 0;
let sumLat = 0;
zone.polygon.forEach(point => {
sumLng += point.y;
sumLat += point.x;
});
const centerLng = sumLng / zone.polygon.length;
const centerLat = sumLat / zone.polygon.length;
this.closePopup();
this.popup = new Popup({
closeButton: true,
closeOnClick: false,
})
.setLngLat([centerLng, centerLat])
.setHTML(`
<div class="delivery-zone">
${zone.name ? `<p><strong>${zone.name}</strong></p>` : ''}
${zone.shippingFee ? `<p>Shipping: ${zone.shippingFee}$</p>` : ''}
${zone.deliverySchedule ? `<p>Delivery: ${zone.deliverySchedule}</p>` : ''}
</div>`)
.addTo(this.map);
}
closePopup(): void
{
if (this.popup) {
this.popup.remove();
this.popup = undefined;
}
}
on(type: string, event: (e: any) => void): void
{
this.map.on(type, event);
}
destroy(): void {
this.map.remove();
}

View File

@ -4,17 +4,40 @@ export interface MapOptions {
styleUrl: string;
tileUrl: string;
}
export interface GeoPoint {
x: number;
y: number;
}
export interface Zone {
id: string;
name?: string;
color?: string;
opacity?: number;
polygon: GeoPoint[];
shippingFee?: number;
deliverySchedule?: string;
}
export type LatLng = [number, number];
export function getLngLat(latLng: LatLng): [number, number] {
return [latLng[1], latLng[0]];
}
export interface IMapAdapter {
export interface IMapAdapter<TMarker = any> {
init(container: HTMLElement, options: MapOptions): void;
setCenter(latLng: LatLng): void;
setZoom(zoom: number): void;
addMarker(latLng: LatLng, options?: { color?: string }): void;
getMarker(id: string): TMarker | undefined;
getAllMarkers(): Record<string, TMarker>;
removeMarker(id?: string): void;
addZone(zone: Zone[]): void;
updateZone(zone: Zone[]): void;
openZonePopup(zone: Zone) : void
closePopup(): void;
destroy(): void;
on(
type: string,
event: (e: any) => void,
): void;
}

View File

@ -1,9 +1,9 @@
import { IMapAdapter, MapOptions, LatLng } from './map-adapter.interface';
import {IMapAdapter, MapOptions, LatLng, getLngLat, Zone} from './map-adapter.interface';
import { LibreAdapter } from './libre-adapter';
import { LeafletAdapter } from './leaflet-adapter';
export class MapFacade implements IMapAdapter {
private readonly adapter: IMapAdapter;
export class MapFacade implements IMapAdapter<any> {
private readonly adapter: IMapAdapter<any>;
private readonly leafletZoomOffset = 1;
constructor(forceRaster: boolean, webglAvailable: boolean) {
@ -30,10 +30,41 @@ export class MapFacade implements IMapAdapter {
this.adapter.setZoom(zoom);
}
addMarker(latLng: LatLng, options?: { color?: string }): void {
addMarker(latLng: LatLng, options: { id?: string; color?: string }): void {
this.adapter.addMarker(latLng, options);
}
getMarker(id: string): any | undefined {
return this.adapter.getMarker(id);
}
getAllMarkers(): Record<string, any> {
return this.adapter.getAllMarkers();
}
removeMarker(id?: string): void {
this.adapter.removeMarker(id);
}
addZone(zones: Zone[]): void
{
this.adapter.addZone(zones);
}
updateZone(zones: Zone[]): void
{
this.adapter.updateZone(zones);
}
openZonePopup(zone: Zone) : void
{
this.adapter.openZonePopup(zone);
}
closePopup(): void
{
this.adapter.closePopup();
}
on(type: string, event: (e: any) => void): void
{
this.adapter.on(type, event);
}
destroy(): void {
this.adapter.destroy();
}

3740
yarn.lock

File diff suppressed because it is too large Load Diff