Files
shopit-front/src/app/shared/components/location-map/location-map.component.ts

212 lines
5.5 KiB
TypeScript

import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
AfterViewInit,
Component,
ElementRef,
Inject,
Input,
OnChanges,
OnDestroy,
PLATFORM_ID,
SimpleChanges,
ViewChild,
} from '@angular/core';
import type { LayerGroup, Map as LeafletMap, LatLngBounds, LatLngTuple, TileLayer } from 'leaflet';
export type MapStyle = 'standard';
const MAP_LAYERS: Record<
MapStyle,
{
url: string;
attribution: string;
maxZoom: number;
}
> = {
standard: {
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
},
};
export interface MapLocation {
id: string | number;
label: string;
latitude: number;
longitude: number;
address?: string;
}
@Component({
selector: 'app-location-map',
templateUrl: './location-map.component.html',
styleUrl: './location-map.component.scss',
})
export class LocationMapComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input({ required: true }) locations: readonly MapLocation[] = [];
@Input() ariaLabel = 'Mapa de ubicaciones';
@Input() fallbackCenter: LatLngTuple = [-32.94682, -60.63932];
@Input() fallbackZoom = 13;
@Input() maxFitZoom = 15;
@Input() mapStyle: MapStyle = 'standard';
@Input() markerColor = '#24a9e0';
@ViewChild('mapContainer', { static: true })
private readonly mapContainer!: ElementRef<HTMLDivElement>;
private leaflet: typeof import('leaflet') | null = null;
private map: LeafletMap | null = null;
private baseLayer: TileLayer | null = null;
private markerLayer: LayerGroup | null = null;
constructor(
@Inject(PLATFORM_ID) private readonly platformId: object,
@Inject(DOCUMENT) private readonly document: Document,
) {}
ngAfterViewInit(): void {
if (isPlatformBrowser(this.platformId)) {
void this.initializeMap();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['mapStyle'] && this.map) {
this.renderBaseLayer();
}
if ((changes['locations'] || changes['markerColor']) && this.map) {
this.renderLocations();
}
}
ngOnDestroy(): void {
this.map?.remove();
this.map = null;
this.baseLayer = null;
this.markerLayer = null;
}
protected directionsUrl(location: MapLocation): string {
const destination = `${location.latitude},${location.longitude}`;
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(destination)}`;
}
private async initializeMap(): Promise<void> {
const leafletModule = await import('leaflet');
const leaflet =
(
leafletModule as unknown as {
default?: typeof import('leaflet');
}
).default ?? leafletModule;
if (!this.mapContainer.nativeElement.isConnected || this.map) {
return;
}
this.leaflet = leaflet;
this.map = leaflet.map(this.mapContainer.nativeElement, {
center: this.fallbackCenter,
zoom: this.fallbackZoom,
scrollWheelZoom: false,
});
this.renderBaseLayer();
this.markerLayer = leaflet.layerGroup().addTo(this.map);
this.renderLocations();
}
private renderBaseLayer(): void {
if (!this.leaflet || !this.map) {
return;
}
if (this.baseLayer) {
this.map.removeLayer(this.baseLayer);
}
const layer = MAP_LAYERS[this.mapStyle];
this.baseLayer = this.leaflet
.tileLayer(layer.url, {
maxZoom: layer.maxZoom,
attribution: layer.attribution,
crossOrigin: true,
})
.addTo(this.map);
}
private renderLocations(): void {
if (!this.leaflet || !this.map || !this.markerLayer) {
return;
}
this.markerLayer.clearLayers();
const validLocations = this.locations.filter(
({ latitude, longitude }) =>
Number.isFinite(latitude) &&
Number.isFinite(longitude) &&
latitude >= -90 &&
latitude <= 90 &&
longitude >= -180 &&
longitude <= 180,
);
if (validLocations.length === 0) {
this.map.setView(this.fallbackCenter, this.fallbackZoom);
return;
}
const bounds: LatLngBounds = this.leaflet.latLngBounds([]);
for (const location of validLocations) {
const coordinates: LatLngTuple = [location.latitude, location.longitude];
bounds.extend(coordinates);
const popup = this.document.createElement('div');
const title = this.document.createElement('strong');
title.textContent = location.label;
popup.append(title);
if (location.address) {
const address = this.document.createElement('div');
address.textContent = location.address;
popup.append(address);
}
const directions = this.document.createElement('a');
directions.href = this.directionsUrl(location);
directions.target = '_blank';
directions.rel = 'noopener noreferrer';
directions.textContent = 'Cómo llegar';
popup.append(directions);
const tooltip = this.document.createElement('span');
tooltip.textContent = location.label;
this.leaflet
.circleMarker(coordinates, {
radius: 8,
color: '#ffffff',
weight: 3,
fillColor: this.markerColor,
fillOpacity: 1,
})
.bindTooltip(tooltip)
.bindPopup(popup)
.addTo(this.markerLayer);
}
if (validLocations.length === 1) {
this.map.setView(bounds.getCenter(), this.maxFitZoom);
} else {
this.map.fitBounds(bounds, {
padding: [32, 32],
maxZoom: this.maxFitZoom,
});
}
}
}