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:
parent
00cc9eab09
commit
46e6f7f44a
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@svrnty/ngx-open-map-wrapper",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"keywords": [
|
||||
"maplibre",
|
||||
"leaflet",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user