map4ng
v1.0.7
Published
Independent Angular implementation of modern map components, inspired by mapcn.dev.
Maintainers
Readme
map4ng
Componentes de mapa para Angular 21+. Basado en MapLibre GL, estilizado con Tailwind CSS 4.
Inspirado en mapcn.dev. Signals nativos, standalone components, cero NgModules.
Índice
- Instalación
- Arranque rápido
- Referencia de API
3.1 MapComponent (<map-gl>)
3.2 MapControlsComponent (<map-controls>)
3.3 MapMarkerComponent (<map-marker>)
3.4 MarkerContentComponent (<marker-content>)
3.5 MarkerLabelComponent (<marker-label>)
3.6 MarkerTooltipComponent (<marker-tooltip>)
3.7 MarkerPopupComponent (<marker-popup>)
3.8 MapPopupComponent (<map-popup>)
3.9 MapRouteComponent (<map-route>)
3.10 MapArcComponent (<map-arc>)
3.11 MapClusterLayerComponent (<map-cluster-layer>)
3.12 MapPolygonComponent (<map-polygon>)
3.13 MapThemeService
3.14 injectMap()
3.15 Tokens de inyección - Sistema de temas
- Uso avanzado
- Guía de estilos
- Recetas
- Resolución de problemas
1. Instalación
Prerrequisitos
- Angular 19+ (Angular 20 recomendado)
- Tailwind CSS 4 — ejecuta
ng add tailwindcssprimero en un proyecto limpio (si se trabaja con Angular 19, es necesaria bien sea una instalación manual o correr el schematic sin haber descargado las dependencias anteriormente)
Método recomendado: ng add
El esquemático configura automáticamente dependencias y estilos:
ng add map4ngEsto realiza automáticamente:
- Añade las dependencias peer al
package.json:maplibre-gl^5.0.0@lucide/angular^1.14.0tailwindcss^4.0.0tailwindcss-animate^1.0.7
- Actualiza
src/styles.cssinyectando:@plugin "tailwindcss-animate"(junto a@import "tailwindcss")@import "map4ng/styles/lib.css"(CSS de la librería)@source "../node_modules/map4ng"(escaneo de clase Tailwind)
- Ejecuta
npm install
Resultado esperado en src/styles.css
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";Estilos base (opcional)
Añade a src/styles.css después del contenido generado:
@layer base {
* {
border-color: var(--color-border, #e2e8f0);
}
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
background-color: var(--color-background, #ffffff);
color: var(--color-foreground, #020817);
transition:
color 300ms,
background-color 300ms;
}
}Fuente Inter (opcional pero recomendado)
Añade en index.html:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />Instalación manual (alternativa)
Si prefieres instalar manualmente, las peer dependencies necesarias son:
npm install map4ng maplibre-gl tailwindcss tailwindcss-animate @lucide/angularY debes añadir manualmente a src/styles.css:
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";Nota: El
@importdelib.csses equivalente a añadirlo enangular.json. Contiene las clases Tailwind precompiladas de la librería y los estilos base de MapLibre GL.
2. Arranque rápido
Tu primer mapa
// app.component.ts
import { Component } from "@angular/core";
import { MapComponent, MapControlsComponent } from "map4ng";
@Component({
selector: "app-root",
imports: [MapComponent, MapControlsComponent],
template: `
<div style="height: 100vh; width: 100vw;">
<map-gl [center]="[-3.7, 40.4]" [zoom]="5">
<map-controls [showFullscreen]="true"></map-controls>
</map-gl>
</div>
`,
})
export class AppComponent {}Mapa con marcadores
import { Component } from "@angular/core";
import { MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent } from "map4ng";
@Component({
selector: "app-root",
imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent],
template: `
<div style="height: 100vh; width: 100vw;">
<map-gl [center]="[-0.12, 51.5]" [zoom]="6">
<map-marker [longitude]="-0.1276" [latitude]="51.5074">
<marker-content>
<div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
</marker-content>
<marker-tooltip>London</marker-tooltip>
</map-marker>
<map-marker [longitude]="2.3522" [latitude]="48.8566">
<marker-content>
<div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
</marker-content>
<marker-tooltip>Paris</marker-tooltip>
</map-marker>
</map-gl>
</div>
`,
})
export class AppComponent {}3. Referencia de API
3.1 MapComponent (<map-gl>)
El componente raíz. Inicializa MapLibre, gestiona temas, y provee los tokens de inyección para componentes hijos.
Selector
<map-gl></map-gl>Modelos (two-way binding con [( )])
| Nombre | Tipo | Default | Descripción |
| --------- | ------------ | -------- | -------------------------- |
| center | LngLatLike | [0, 0] | Centro [longitud, latitud] |
| zoom | number | 0 | Nivel de zoom |
| bearing | number | 0 | Rotación (grados) |
| pitch | number | 0 | Inclinación (grados) |
Estos son
model()signals. Se sincronizan bidireccionalmente con el mapa: al navegar, se actualizan automáticamente. Usa[( )]o binding unidireccional + event listener.
Inputs
| Nombre | Tipo | Default | Descripción |
| ------------------- | --------------------------------------------- | -------------- | --------------------------------------------------------- |
| projection | ProjectionSpecification | — | Proyección. Ej: { type: 'globe' } |
| theme | 'light' \| 'dark' | auto | Forzar tema. Si no se especifica, detecta automáticamente |
| styles | { light?: StyleOption, dark?: StyleOption } | Carto defaults | Estilos de mapa personalizados |
| loading | boolean | false | Mostrar overlay de carga |
| className | string | '' | Clases CSS adicionales |
| scrollZoom | boolean \| Options | true | Zoom con scroll |
| dragPan | boolean \| Options | true | Arrastre con ratón |
| dragRotate | boolean \| Options | true | Rotación con ratón |
| touchZoomRotate | boolean \| Options | true | Zoom/rotación táctil |
| doubleClickZoom | boolean | true | Zoom con doble click |
| keyboard | boolean | true | Atajos de teclado |
| boxZoom | boolean | true | Zoom con rectángulo |
| interactive | boolean | true | Interactividad general |
| renderWorldCopies | boolean | false | Múltiples copias del mundo |
| mapOptions | MapOptions | {} | Opciones extra de MapLibre (minZoom, maxZoom, etc.) |
Outputs
| Nombre | Tipo | Descripción |
| ------------------ | ------------- | -------------------------------- |
| onViewportChange | MapViewport | Emite en cada cambio de viewport |
Types
type MapViewport = {
center: [number, number]; // [longitude, latitude]
zoom: number;
bearing: number;
pitch: number;
};
type MapStyleOption = string | StyleSpecification;
type MapRef = MapLibreMap;Ejemplo: viewport controlado (two-way binding)
@Component({
template: ` <map-gl [(center)]="center" [(zoom)]="zoom" [(bearing)]="bearing" [(pitch)]="pitch" (onViewportChange)="onChange($event)"></map-gl> `,
})
class Example {
center = signal<LngLatLike>([-74.006, 40.7128]);
zoom = signal(8);
bearing = signal(0);
pitch = signal(0);
onChange(vp: MapViewport) {
console.log("Viewport:", vp);
}
}También puedes escuchar solo
(onViewportChange)para obtener{ center, zoom, bearing, pitch }en cada movimiento.
Ejemplo: mapa 3D (globo)
<map-gl [center]="[-113, 43]" [zoom]="3.2" [projection]="{ type: 'globe' }" [pitch]="24" [mapOptions]="{ minZoom: 1.2, maxZoom: 8 }"></map-gl>3.2 MapControlsComponent (<map-controls>)
Controles de mapa: zoom, brújula, geolocalización, pantalla completa.
Selector
<map-controls></map-controls>Inputs
| Nombre | Tipo | Default | Descripción |
| ---------------- | -------------------------------------------------------------- | ---------------- | -------------------------------- |
| position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Posición en el mapa |
| showZoom | boolean | true | Botones de zoom +/- |
| showCompass | boolean | false | Botón de brújula (reset bearing) |
| showLocate | boolean | false | Botón de geolocalización |
| showFullscreen | boolean | false | Botón de pantalla completa |
| className | string | '' | Clases CSS adicionales |
Outputs
| Nombre | Tipo | Descripción |
| ---------- | ----------------------------------------- | ----------------------------- |
| onLocate | { longitude: number, latitude: number } | Emite coordenadas del usuario |
3.3 MapMarkerComponent (<map-marker>)
Coloca un marcador en el mapa. Soporta contenido personalizado, popup, tooltip y label.
Selector
<map-marker></map-marker>Inputs
| Nombre | Tipo | Default | Descripción |
| ------------------- | ------------------------------- | ------------ | --------------------------- |
| longitude | number | required | Longitud |
| latitude | number | required | Latitud |
| draggable | boolean | false | Permitir arrastre |
| offset | PointLike | — | Desplazamiento del marcador |
| rotation | number | 0 | Rotación (grados) |
| rotationAlignment | 'map' \| 'viewport' \| 'auto' | 'auto' | Alineación de rotación |
| pitchAlignment | 'map' \| 'viewport' \| 'auto' | 'auto' | Alineación de inclinación |
| anchor | PointAnchor | 'center' | Punto de anclaje |
| color | string | '#3b82f6' | Color del dot por defecto |
| scale | number | 1 | Escala del dot por defecto |
| className | string | '' | Clases CSS adicionales |
| options | MarkerOptions | {} | Opciones extra de MapLibre |
Outputs
| Nombre | Tipo | Descripción |
| -------------- | ------------------------------ | -------------------- |
| onClick | MouseEvent | Click en el marcador |
| onMouseEnter | MouseEvent | Ratón entra |
| onMouseLeave | MouseEvent | Ratón sale |
| onDragStart | { lng: number, lat: number } | Inicio de arrastre |
| onDrag | { lng: number, lat: number } | Durante arrastre |
| onDragEnd | { lng: number, lat: number } | Fin de arrastre |
Sub-componentes
Dentro de <map-marker> puedes usar:
<marker-content>— contenido visual del marcador<marker-label>— etiqueta de texto<marker-tooltip>— tooltip al hacer hover<marker-popup>— popup al hacer click
3.4 MarkerContentComponent (<marker-content>)
Envuelve el contenido visual del marcador. Si no se proporciona, muestra un círculo azul por defecto.
Inputs
| Nombre | Tipo | Default | Descripción |
| ----------- | -------- | ------- | ---------------------- |
| className | string | '' | Clases CSS adicionales |
3.5 MarkerLabelComponent (<marker-label>)
Etiqueta de texto posicionada encima o debajo del marcador.
Inputs
| Nombre | Tipo | Default | Descripción |
| ----------- | ------------------- | ------- | ----------------------------- |
| position | 'top' \| 'bottom' | 'top' | Posición relativa al marcador |
| className | string | '' | Clases CSS adicionales |
3.6 MarkerTooltipComponent (<marker-tooltip>)
Tooltip que aparece al hacer hover sobre el marcador.
Inputs
| Nombre | Tipo | Default | Descripción |
| ----------- | -------------- | ------- | -------------------------- |
| className | string | '' | Clases CSS adicionales |
| options | PopupOptions | {} | Opciones de MapLibre Popup |
Comportamiento
- Se muestra al hacer
mouseenteren el marcador - Se oculta al hacer
mouseleave - Fondo oscuro, texto claro
- Se cierra automáticamente al hacer click en cualquier parte
3.7 MarkerPopupComponent (<marker-popup>)
Popup que se abre al hacer click en el marcador. Adjunto al marcador.
Inputs
| Nombre | Tipo | Default | Descripción |
| ------------- | -------------- | ------- | --------------------------- |
| closeButton | boolean | false | Mostrar botón X para cerrar |
| className | string | '' | Clases CSS adicionales |
| options | PopupOptions | {} | Opciones de MapLibre Popup |
Outputs
| Nombre | Tipo | Descripción |
| --------- | ------ | ------------------------------- |
| onClose | void | Emite cuando el popup se cierra |
3.8 MapPopupComponent (<map-popup>)
Popup independiente posicionado en coordenadas específicas. No requiere un marcador.
Selector
<map-popup></map-popup>Inputs
| Nombre | Tipo | Default | Descripción |
| ------------- | -------------- | ------------ | -------------------------- |
| longitude | number | required | Longitud |
| latitude | number | required | Latitud |
| closeButton | boolean | false | Mostrar botón X |
| className | string | '' | Clases CSS adicionales |
| options | PopupOptions | {} | Opciones de MapLibre Popup |
Outputs
| Nombre | Tipo | Descripción |
| --------- | ------ | ------------------------------- |
| onClose | void | Emite cuando el popup se cierra |
3.9 MapRouteComponent (<map-route>)
Dibuja una línea conectando coordenadas.
Inputs
| Nombre | Tipo | Default | Descripción |
| ------------- | -------------------- | ------------ | ------------------------------------ |
| coordinates | [number, number][] | required | Pares [lng, lat] |
| id | string | auto | Identificador de capa |
| color | string | '#3b82f6' | Color de línea |
| width | number | 3 | Grosor (px) |
| opacity | number | 0.8 | Opacidad (0-1) |
| dashArray | number[] | — | Patrón de guiones [largo, espacio] |
| interactive | boolean | true | Habilitar eventos click/hover |
Outputs
| Nombre | Tipo | Descripción |
| -------------- | ------ | ------------------- |
| onClick | void | Ruta clickeada |
| onMouseEnter | void | Ratón entra en ruta |
| onMouseLeave | void | Ratón sale de ruta |
3.10 MapArcComponent (<map-arc>)
Dibuja curvas Bézier entre pares de coordenadas. Ideal para rutas aéreas, marítimas, o conexiones origen-destino.
Inputs
| Nombre | Tipo | Default | Descripción |
| ------------- | --------------- | -------------------------------------------------------------- | ------------------------------------ |
| data | MapArcDatum[] | required | Array de arcos |
| id | string | auto | Identificador de capa |
| curvature | number | 0.2 | Curvatura (0 = recta) |
| samples | number | 64 | Puntos de interpolación |
| paint | LinePaint | { line-color: '#4285F4', line-width: 2, line-opacity: 0.85 } | Estilo de línea |
| hoverPaint | LinePaint | — | Estilo al hacer hover |
| layout | LineLayout | { line-join: 'round', line-cap: 'round' } | Layout de línea |
| interactive | boolean | true | Habilitar hover/click |
| beforeId | string | — | ID de capa antes de la cual insertar |
Types
interface MapArcDatum {
id: string | number;
from: [number, number];
to: [number, number];
[key: string]: any; // propiedades extra usables en paint expressions
}
interface MapArcEvent<T> {
arc: T;
longitude: number;
latitude: number;
originalEvent: MapMouseEvent;
}Outputs
| Nombre | Tipo | Descripción |
| --------- | --------------------- | --------------------------- |
| onClick | MapArcEvent | Arco clickeado |
| onHover | MapArcEvent \| null | Hover cambió (null = salió) |
Ejemplo: arcos con color por categoría
@Component({
template: `
<map-gl [center]="[20, 20]" [zoom]="0.8">
<map-arc
[data]="lanes"
[paint]="{
'line-color': ['match', ['get', 'mode'], 'air', '#a78bfa', 'sea', '#34d399', '#888'],
'line-width': 1.5,
}"
[hoverPaint]="{ 'line-width': 3, 'line-opacity': 1 }"
></map-arc>
</map-gl>
`,
})
class Example {
lanes = [
{
id: "1",
from: [121.4, 31.2],
to: [-118.2, 34.0],
mode: "sea",
volume: "24.8k",
},
{
id: "2",
from: [151.2, -33.8],
to: [139.6, 35.6],
mode: "air",
volume: "640t",
},
];
}3.11 MapClusterLayerComponent (<map-cluster-layer>)
Visualiza grandes conjuntos de datos con clustering automático.
Inputs
| Nombre | Tipo | Default | Descripción |
| ------------------- | ------------------------------------- | ----------------------------------- | -------------------------------- |
| data | string \| GeoJSON.FeatureCollection | required | Datos GeoJSON o URL |
| clusterMaxZoom | number | 14 | Zoom máximo para clustering |
| clusterRadius | number | 50 | Radio de cluster (px) |
| clusterColors | [string, string, string] | ['#51bbd6', '#f1f075', '#f28cb1'] | Colores [pequeño, medio, grande] |
| clusterThresholds | [number, number] | [100, 750] | Umbrales de cantidad de puntos |
| pointColor | string | '#3b82f6' | Color de puntos individuales |
Outputs
| Nombre | Tipo | Descripción |
| ---------------- | ---------------------------------------- | -------------------------- |
| onPointClick | { feature, coordinates } | Punto individual clickeado |
| onClusterClick | { clusterId, coordinates, pointCount } | Cluster clickeado |
Comportamiento por defecto
- Click en cluster: hace zoom para expandir el cluster
- Click en punto: emite
onPointClick - Si
onClusterClicktiene listeners, no hace zoom automático
3.12 MapPolygonComponent (<map-polygon>)
Dibuja áreas delimitadas por polígonos GeoJSON con relleno y borde configurables.
Inputs
| Nombre | Tipo | Default | Descripción |
| --------------- | ---------------------- | ------------ | ------------------------------------------ |
| coordinates | [number, number][][] | required | Array de anillos de coordenadas [lng, lat] |
| id | string | auto | Identificador de capa |
| fillColor | string | '#3b82f6' | Color de relleno |
| fillOpacity | number | 0.2 | Opacidad de relleno |
| strokeColor | string | '#3b82f6' | Color del borde |
| strokeWidth | number | 2 | Grosor del borde (px) |
| strokeOpacity | number | 0.8 | Opacidad del borde |
| dashArray | number[] | — | Patrón de guiones [largo, espacio] |
| interactive | boolean | true | Habilitar eventos click/hover |
| beforeId | string | — | Insertar antes de otra capa |
Outputs
| Nombre | Tipo | Descripción |
| -------------- | --------------- | ------------------ |
| onClick | MapMouseEvent | Polígono clickeado |
| onMouseEnter | MapMouseEvent | Ratón entra |
| onMouseLeave | MapMouseEvent | Ratón sale |
Ejemplo: delimitar un área
<map-gl [center]="[-75.498, 10.397]" [zoom]="16" [pitch]="45">
<map-polygon [coordinates]="[areaCoords]" fillColor="#ef4444" [fillOpacity]="0.20" strokeColor="#dc2626" [strokeWidth]="2.5"></map-polygon>
</map-gl>areaCoords: [number, number][] = [
[-75.4985, 10.3988], [-75.4968, 10.3988],
[-75.4960, 10.3971], [-75.4964, 10.3963],
[-75.4971, 10.3957], [-75.4980, 10.3954],
[-75.4989, 10.3955], [-75.4997, 10.3961],
[-75.4985, 10.3988], // cerrar el polígono
];Ejemplo: múltiples áreas con dash
<map-polygon [coordinates]="[zonaA, zonaB]" fillColor="#22c55e" strokeColor="#16a34a" [dashArray]="[4, 2]"></map-polygon>3.13 MapThemeService
Servicio providedIn: 'root' que detecta y gestiona el tema claro/oscuro.
API
@Injectable({ providedIn: "root" })
class MapThemeService {
theme: Signal<"light" | "dark">; // Tema actual
toggleTheme(): void; // Alternar y persistir vía clase 'dark' en <html>
setTheme(theme: Theme): void; // Forzar tema específico
}Funcionamiento
- Detecta la clase
darkendocument.documentElement - Si no hay clase, usa
prefers-color-scheme: dark - Observa cambios en la clase y en la media query
toggleTheme()añade/quita la clasedark
Uso
import { inject } from '@angular/core';
import { MapThemeService } from 'map4ng';
@Component({ ... })
class MyComponent {
themeService = inject(MapThemeService);
toggleTheme() {
this.themeService.toggleTheme();
// El mapa <map-gl> reacciona automáticamente al cambio de tema
}
}3.14 injectMap()
Función para acceder a la instancia del mapa y su estado de carga desde cualquier componente hijo de <map-gl>.
API
function injectMap(): {
map: Signal<MapLibreMap | null>;
isLoaded: Signal<boolean>;
};Uso
import { Component, effect, signal, inject } from '@angular/core';
import { injectMap } from 'map4ng';
@Component({
selector: 'app-zoom-display',
template: `<p>Zoom: {{ zoom() }}</p>`,
})
class ZoomDisplay {
{ map, isLoaded } = injectMap(); // destructurable!
zoom = signal(0);
constructor() {
effect((onCleanup) => {
const m = this.map();
if (!m) return;
const handler = () => this.zoom.set(m.getZoom());
m.on('zoom', handler);
onCleanup(() => m.off('zoom', handler));
});
}
}3.15 Tokens de inyección
Tokens subyacentes
injectMap() es un wrapper de conveniencia. También puedes inyectar los tokens directamente:
import { MAP_INSTANCE, MAP_LOADED, MARKER_INSTANCE } from "map4ng";Tokens de inyección
| Token | Tipo | Proveído por | Descripción |
| ----------------- | ------------------------ | -------------- | ------------------------------------ |
| MAP_INSTANCE | Signal<Map \| null> | <map-gl> | Instancia del mapa |
| MAP_LOADED | Signal<boolean> | <map-gl> | Si el estilo del mapa ha cargado |
| MARKER_INSTANCE | Signal<Marker \| null> | <map-marker> | Instancia del marcador |
| MAP_CONFIG | MapConfig | root | Configuración de estilos por defecto |
MAP_CONFIG
El token MAP_CONFIG define los estilos de mapa por defecto para tema claro/oscuro. Se puede sobreescribir en app.config.ts:
import { MAP_CONFIG } from "map4ng";
export const appConfig: ApplicationConfig = {
providers: [
{
provide: MAP_CONFIG,
useValue: {
lightStyle: "https://tiles.openfreemap.org/styles/bright",
darkStyle: "https://tiles.openfreemap.org/styles/liberty",
},
},
],
};4. Sistema de temas
Cómo funciona
El sistema de temas de la librería usa CSS custom properties definidas en dos capas:
:root— variables para tema claro.dark— variables para tema oscuro (se añade al<html>)
Las variables definidas son:
:root {
--color-background: #ffffff;
--color-foreground: #020817;
--color-card: #ffffff;
--color-card-foreground: #020817;
--color-popover: #ffffff;
--color-popover-foreground: #020817;
--color-primary: #0f172a;
--color-primary-foreground: #f8fafc;
--color-secondary: #f1f5f9;
--color-secondary-foreground: #0f172a;
--color-muted: #f1f5f9;
--color-muted-foreground: #64748b;
--color-accent: #f1f5f9;
--color-accent-foreground: #0f172a;
--color-border: #e2e8f0;
--color-ring: #3b82f6;
}
.dark {
--color-background: #020817;
--color-foreground: #f8fafc;
/* ... resto de variables oscuras ... */
}Personalizar colores
Sobrescribe las variables en tu styles.css:
@layer base {
:root {
--color-primary: #2563eb; /* tu azul */
--color-background: #fafafa; /* tu fondo */
--color-border: #d4d4d8; /* tu borde */
}
.dark {
--color-primary: #3b82f6;
--color-background: #0a0a0a;
--color-border: #27272a;
}
}Cambiar tiles del mapa
<!-- OpenStreetMap -->
<map-gl [styles]="{ light: osmUrl, dark: osmUrl }"></map-gl>
<!-- MapTiler (necesita API key) -->
<map-gl [styles]="{ light: maptilerUrl, dark: maptilerDarkUrl }"></map-gl>5. Uso avanzado
Añadir capas personalizadas
Patrón para añadir fuentes y capas GeoJSON, heatmap, etc.:
import { Component, effect, inject, OnDestroy } from "@angular/core";
import { MAP_INSTANCE, MAP_LOADED } from "map4ng";
import type { Map } from "maplibre-gl";
@Component({
selector: "app-custom-layer",
template: ``, // sin template
})
export class CustomLayerComponent implements OnDestroy {
private map = inject(MAP_INSTANCE);
private loaded = inject(MAP_LOADED);
private sourceId = "my-source";
private layerId = "my-layer";
constructor() {
effect((onCleanup) => {
const m = this.map();
if (!this.loaded() || !m) return;
m.addSource(this.sourceId, {
type: "geojson",
data: "https://example.com/data.geojson",
});
m.addLayer({
id: this.layerId,
type: "circle",
source: this.sourceId,
paint: { "circle-color": "#ff0000", "circle-radius": 8 },
});
onCleanup(() => {
try {
m.removeLayer(this.layerId);
} catch {}
try {
m.removeSource(this.sourceId);
} catch {}
});
});
}
ngOnDestroy() {
const m = this.map();
if (m) {
try {
m.removeLayer(this.layerId);
} catch {}
try {
m.removeSource(this.sourceId);
} catch {}
}
}
}Uso:
<map-gl [center]="[2, 40]" [zoom]="4">
<app-custom-layer></app-custom-layer>
</map-gl>Acceso directo al mapa
Para operaciones imperativas:
const { map } = injectMap();
// Volar a una ubicación
effect(() => {
const m = this.map();
m?.flyTo({ center: [lng, lat], zoom: 14, duration: 2000 });
});
// Añadir un evento de mapa
effect((onCleanup) => {
const m = this.map();
if (!m) return;
const handler = (e) => console.log("click", e.lngLat);
m.on("click", handler);
onCleanup(() => m.off("click", handler));
});SSR (Server-Side Rendering)
Todos los componentes verifican isPlatformBrowser() antes de interactuar con el DOM. En SSR, los componentes se renderizan como placeholders. El mapa se inicializa solo en el cliente tras la hidratación.
// No se necesita configuración especial
// El mapa se inicializa automáticamente en el navegador6. Guía de estilos
Clases Tailwind disponibles
La librería exporta un CSS precompilado con ~200 clases Tailwind usadas por sus componentes. Las clases de color personalizadas incluyen:
| Clase | Propiedad CSS |
| ------------------------- | ------------------------------------------- |
| bg-background | background-color: var(--color-background) |
| text-foreground | color: var(--color-foreground) |
| bg-popover | background-color: var(--color-popover) |
| text-popover-foreground | color: var(--color-popover-foreground) |
| bg-muted | background-color: var(--color-muted) |
| text-muted-foreground | color: var(--color-muted-foreground) |
| bg-accent | background-color: var(--color-accent) |
| border-border | border-color: var(--color-border) |
| bg-primary | background-color: var(--color-primary) |
Todas las variantes (hover:, focus-visible:, dark:, disabled:) también están incluidas.
Estilizar marcadores
<!-- Dot simple -->
<marker-content>
<div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
</marker-content>
<!-- Dot con texto -->
<marker-content>
<div class="flex size-6 items-center justify-center rounded-full border-2 border-white bg-emerald-500 text-[10px] font-bold text-white shadow-lg">1</div>
</marker-content>
<!-- Icono personalizado -->
<marker-content>
<div class="text-2xl">📍</div>
</marker-content>
<!-- Precio (estilo inmobiliario) -->
<marker-content>
<div class="px-2 py-1 bg-white border border-border rounded-full text-xs font-bold shadow-sm">$1.2M</div>
</marker-content>Estilizar popups
<marker-popup [closeButton]="true" className="w-64 p-0 overflow-hidden">
<div class="flex flex-col">
<div class="h-32 bg-muted relative">
<!-- imagen -->
</div>
<div class="p-4 flex flex-col gap-2">
<h3 class="font-bold text-lg">Título</h3>
<p class="text-xs text-muted-foreground">Descripción</p>
</div>
</div>
</marker-popup>Layout del mapa
<!-- Pantalla completa -->
<div class="h-screen w-full">
<map-gl ...></map-gl>
</div>
<!-- Tarjeta con altura fija -->
<div class="h-[500px] w-full border rounded-xl overflow-hidden shadow-lg">
<map-gl ...></map-gl>
</div>
<!-- Mapa con overlay -->
<div class="relative h-[600px] w-full">
<map-gl ...></map-gl>
<div class="absolute top-4 left-4 z-10 bg-background/80 backdrop-blur rounded-lg border p-3 shadow">
<!-- Controles o info -->
</div>
</div>7. Recetas
Mapa analytics con marcadores y tarjetas
@Component({
imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MapControlsComponent],
template: `
<div class="h-[600px] w-full border rounded-2xl overflow-hidden shadow-2xl relative">
<map-gl [center]="[-2, 16]" [zoom]="1.5" [scrollZoom]="false">
<map-controls [showFullscreen]="true"></map-controls>
@for (loc of locations; track loc.city) {
<map-marker [longitude]="loc.lng" [latitude]="loc.lat">
<marker-content>
<div class="rounded-full bg-blue-500/70" [style.width.px]="loc.size * 3" [style.height.px]="loc.size * 3"></div>
</marker-content>
<marker-tooltip>
<p class="font-medium">{{ loc.city }}</p>
<p class="mt-0.5">{{ loc.size }} active users</p>
</marker-tooltip>
</map-marker>
}
</map-gl>
<!-- Overlay de analytics -->
<div class="absolute top-4 left-4 z-10 w-60 bg-background/80 backdrop-blur border rounded-xl p-4 shadow-sm">
<p class="text-[10px] tracking-wider uppercase opacity-60">Users last 7 days</p>
<p class="text-3xl font-semibold">3,803</p>
<div class="mt-2 flex items-center gap-1 text-xs text-emerald-500">+12.5% vs previous 7 days</div>
</div>
</div>
`,
})
class AnalyticsMap {
locations = [
{ city: "San Francisco", lng: -122.41, lat: 37.77, size: 16 },
{ city: "New York", lng: -74.0, lat: 40.71, size: 15 },
{ city: "London", lng: -0.12, lat: 51.5, size: 14 },
{ city: "Tokyo", lng: 139.69, lat: 35.68, size: 12 },
];
}Ruta de delivery con progreso
@Component({
imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MapRouteComponent],
template: `
<div class="relative h-[500px] w-full border rounded-xl overflow-hidden shadow-lg">
<map-gl [center]="[-74.006, 40.7128]" [zoom]="13" [scrollZoom]="false">
<map-route [coordinates]="path" color="#3b82f6" [width]="5" [opacity]="0.8"></map-route>
<!-- Tienda -->
<map-marker [longitude]="-74.015" [latitude]="40.705">
<marker-content>
<div class="p-1 bg-background border-2 border-blue-500 rounded-lg shadow-md text-blue-500">🏪</div>
</marker-content>
<marker-tooltip>Store</marker-tooltip>
</map-marker>
<!-- Courier (con ping animado) -->
<map-marker [longitude]="-73.998" [latitude]="40.715">
<marker-content>
<div class="relative flex items-center justify-center">
<div class="absolute size-8 bg-blue-500/30 rounded-full animate-ping"></div>
<div class="size-4 bg-blue-600 rounded-full border-2 border-white shadow-lg"></div>
</div>
</marker-content>
<marker-tooltip>Courier: In transit</marker-tooltip>
</map-marker>
<!-- Destino -->
<map-marker [longitude]="-73.985" [latitude]="40.725">
<marker-content>
<div class="p-1 bg-background border-2 border-orange-500 rounded-full shadow-md text-orange-500">📍</div>
</marker-content>
<marker-tooltip>Delivery Address</marker-tooltip>
</map-marker>
</map-gl>
<div class="absolute bottom-4 left-4 bg-background/90 backdrop-blur p-3 rounded-lg border shadow-sm z-10">
<h3 class="font-bold text-sm">Order #4292</h3>
<p class="text-xs opacity-60">Estimated delivery: 12 mins</p>
</div>
</div>
`,
})
class DeliveryTracker {
path: [number, number][] = [
[-74.015, 40.705],
[-74.01, 40.71],
[-74.005, 40.715],
[-73.998, 40.715],
[-73.99, 40.72],
[-73.985, 40.725],
];
}Mapa inmobiliario con popups de propiedades
@Component({
imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MarkerPopupComponent, MapControlsComponent],
template: `
<div class="h-[600px] w-full border rounded-2xl overflow-hidden relative shadow-2xl">
<map-gl [center]="[-118.24, 34.05]" [zoom]="11.5" [scrollZoom]="false">
<map-controls position="bottom-right" [showCompass]="true"></map-controls>
@for (p of properties; track p.id) {
<map-marker [longitude]="p.lng" [latitude]="p.lat">
<marker-content>
<div class="px-2 py-1 bg-white border border-border shadow-sm rounded-full text-[10px] font-bold">
{{ p.price }}
</div>
</marker-content>
<marker-tooltip className="border shadow-lg p-2 rounded-md">
<span class="font-bold text-xs">{{ p.price }}</span>
<span class="text-[10px] opacity-60 ml-2">{{ p.type }} · {{ p.beds }} bds</span>
</marker-tooltip>
<marker-popup [closeButton]="true" className="w-64 p-0 overflow-hidden">
<div class="flex flex-col">
<div class="h-32 bg-muted relative">
<div class="absolute top-2 left-2 px-2 py-0.5 bg-white/80 backdrop-blur rounded text-[10px] font-medium uppercase">
{{ p.type }}
</div>
</div>
<div class="p-4 flex flex-col gap-2">
<div class="flex justify-between">
<h3 class="font-bold text-lg">{{ p.price }}</h3>
<div class="flex gap-2 text-xs opacity-60">
<span>{{ p.beds }} bds</span>
<span>{{ p.baths }} ba</span>
</div>
</div>
<p class="text-xs opacity-60">{{ p.sqft }}</p>
</div>
</div>
</marker-popup>
</map-marker>
}
</map-gl>
</div>
`,
})
class RealEstateMap {
properties = [
{
id: "1",
lat: 34.0522,
lng: -118.2437,
price: "$1.2M",
type: "House",
beds: 3,
baths: 2,
sqft: "2,100 sqft",
},
{
id: "2",
lat: 34.0622,
lng: -118.2537,
price: "$850K",
type: "Condo",
beds: 2,
baths: 1,
sqft: "1,200 sqft",
},
{
id: "3",
lat: 34.0422,
lng: -118.2337,
price: "$2.4M",
type: "Modern",
beds: 4,
baths: 3.5,
sqft: "3,800 sqft",
},
];
}Heatmap con capa personalizada
import { Component, effect, inject, OnDestroy } from "@angular/core";
import { MAP_INSTANCE, MAP_LOADED } from "map4ng";
const DATA_URL = "https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson";
@Component({
selector: "app-earthquake-heatmap",
template: ``,
})
export class EarthquakeHeatmapComponent implements OnDestroy {
private map = inject(MAP_INSTANCE);
private loaded = inject(MAP_LOADED);
private sourceId = "eq-source";
private heatId = "eq-heat";
private pointId = "eq-points";
constructor() {
effect((onCleanup) => {
const m = this.map();
if (!this.loaded() || !m) return;
this.addLayers(m);
onCleanup(() => this.removeLayers(m));
});
}
private addLayers(m: any) {
m.addSource(this.sourceId, { type: "geojson", data: DATA_URL });
m.addLayer({
id: this.heatId,
type: "heatmap",
source: this.sourceId,
maxzoom: 9,
paint: {
"heatmap-weight": ["interpolate", ["linear"], ["get", "mag"], 0, 0, 6, 1],
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 9, 3],
"heatmap-color": ["interpolate", ["linear"], ["heatmap-density"], 0, "rgba(33,102,172,0)", 0.2, "rgb(103,169,207)", 0.4, "rgb(209,229,240)", 0.6, "rgb(253,219,199)", 0.8, "rgb(239,138,98)", 1, "rgb(178,24,43)"],
"heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20],
"heatmap-opacity": ["interpolate", ["linear"], ["zoom"], 7, 1, 9, 0],
},
});
}
private removeLayers(m: any) {
try {
m.removeLayer(this.pointId);
} catch {}
try {
m.removeLayer(this.heatId);
} catch {}
try {
m.removeSource(this.sourceId);
} catch {}
}
ngOnDestroy() {
const m = this.map();
if (m) this.removeLayers(m);
}
}8. Resolución de problemas
Los estilos Tailwind no se ven
Síntoma: Los controles aparecen sin fondo, texto sin color, bordes invisibles.
Causa: El CSS precompilado de la librería no se está cargando.
Solución: Verifica que src/styles.css incluya el import de la librería después de @plugin "tailwindcss-animate":
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";Si usaste ng add map4ng, estas líneas se añaden automáticamente. Si instalaste manualmente, verifica que @import "tailwindcss" exista y que el @import de lib.css esté presente.
El mapa no se renderiza
Síntoma: Pantalla en blanco, sin mapa.
Causas posibles:
maplibre-glno está instalado (npm install maplibre-gl)- El
@import "map4ng/styles/lib.css"no está ensrc/styles.css - El componente no se importó correctamente (verifica los imports en tu componente)
- No hay altura definida en el contenedor
Solución:
<!-- Asegúrate de dar altura explícita -->
<div style="height: 100vh; width: 100vw;">
<map-gl ...></map-gl>
</div>Los marcadores no aparecen
Síntoma: El mapa se ve pero los marcadores no.
Causas:
- Latitud/Longitud intercambiadas (primero va lng, luego lat)
- El mapa aún no ha cargado (los marcadores se añaden cuando el mapa está listo)
- Coordenadas fuera del viewport visible
Solución:
<!-- Correcto: [longitude, latitude] -->
<map-marker [longitude]="-74.006" [latitude]="40.7128">
<!-- Incorrecto: latitud primero -->
<map-marker [longitude]="40.7128" [latitude]="-74.006"></map-marker
></map-marker>Tooltip aparece como línea vertical
Síntoma: El tooltip se muestra pero es extremadamente estrecho.
Causa: El contenido del tooltip colapsa.
Solución: Ya corregido en la librería (v1.0.0+). Si persiste, añade className="whitespace-nowrap" al <marker-tooltip>.
Error "maplibre-gl is not ESM"
Síntoma: Warning de build: CommonJS or AMD dependencies can cause optimization bailouts.
Causa: maplibre-gl se distribuye como CJS/AMD.
Solución: Es un warning inofensivo. No afecta al funcionamiento. Para eliminarlo, añade a angular.json:
"build": {
"options": {
"allowedCommonJsDependencies": ["maplibre-gl"]
}
}La fuente Inter no carga
Síntoma: El texto usa la fuente sans-serif del sistema.
Solución: Añade el link de Google Fonts en src/index.html:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />Y añade a src/styles.css:
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
}Resumen de componentes
| Selector | Componente | Descripción |
| --------------------- | -------------------------- | ------------------------------------ |
| <map-gl> | MapComponent | Contenedor principal del mapa |
| <map-controls> | MapControlsComponent | Zoom, brújula, localizar, fullscreen |
| <map-marker> | MapMarkerComponent | Marcador en coordenadas |
| <marker-content> | MarkerContentComponent | Contenido visual del marcador |
| <marker-popup> | MarkerPopupComponent | Popup al hacer click en marcador |
| <marker-tooltip> | MarkerTooltipComponent | Tooltip al hover en marcador |
| <marker-label> | MarkerLabelComponent | Etiqueta de texto del marcador |
| <map-popup> | MapPopupComponent | Popup standalone en coordenadas |
| <map-route> | MapRouteComponent | Línea entre coordenadas |
| <map-arc> | MapArcComponent | Curva Bézier entre pares de puntos |
| <map-cluster-layer> | MapClusterLayerComponent | Capa de clustering GeoJSON |
| <map-polygon> | MapPolygonComponent | Polígono GeoJSON con relleno y borde |
| Servicio/Función | Descripción |
| ----------------- | -------------------------------------------- |
| MapThemeService | Servicio de detección y toggle de tema |
| injectMap() | Acceso reactivo a la instancia del mapa |
| MAP_INSTANCE | Token de inyección: instancia del mapa |
| MAP_LOADED | Token de inyección: estado de carga |
| MARKER_INSTANCE | Token de inyección: instancia del marcador |
| MAP_CONFIG | Token de inyección: configuración de estilos |
