@mounaji_npm/geo-map 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mounajigeomap.es.js +100 -0
- package/dist/mounajigeomap.es.js.map +1 -0
- package/dist/mounajigeomap.umd.cjs +2 -0
- package/dist/mounajigeomap.umd.cjs.map +1 -0
- package/dist/types/GeoMap.d.ts +38 -0
- package/dist/types/GeoMap.d.ts.map +1 -0
- package/dist/types/estilo.d.ts +12 -0
- package/dist/types/estilo.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +42 -0
- package/src/GeoMap.tsx +151 -0
- package/src/estilo.ts +35 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as j } from "react/jsx-runtime";
|
|
3
|
+
import { useRef as s, useEffect as m } from "react";
|
|
4
|
+
import f from "maplibre-gl";
|
|
5
|
+
const M = {
|
|
6
|
+
version: 8,
|
|
7
|
+
sources: {
|
|
8
|
+
osm: {
|
|
9
|
+
type: "raster",
|
|
10
|
+
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
|
|
11
|
+
tileSize: 256,
|
|
12
|
+
attribution: "© OpenStreetMap contributors"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
layers: [{ id: "osm", type: "raster", source: "osm" }]
|
|
16
|
+
}, O = {
|
|
17
|
+
con_dueno: "#16a34a",
|
|
18
|
+
callejero: "#ea580c",
|
|
19
|
+
sin_verificar: "#6b7280"
|
|
20
|
+
};
|
|
21
|
+
function R(r, c) {
|
|
22
|
+
if (r) {
|
|
23
|
+
const n = (c == null ? void 0 : c[r]) ?? O[r];
|
|
24
|
+
if (n) return n;
|
|
25
|
+
}
|
|
26
|
+
return "#6b7280";
|
|
27
|
+
}
|
|
28
|
+
function E({
|
|
29
|
+
centro: r,
|
|
30
|
+
zoom: c = 14,
|
|
31
|
+
pins: n = [],
|
|
32
|
+
zonas: o = [],
|
|
33
|
+
onTap: i,
|
|
34
|
+
seleccionado: l = null,
|
|
35
|
+
alto: w = "70vh",
|
|
36
|
+
estilo: S = M,
|
|
37
|
+
coloresEstado: b
|
|
38
|
+
}) {
|
|
39
|
+
const g = s(null), a = s(null), d = s(!1), L = s([]), y = s(null), p = s(i);
|
|
40
|
+
return p.current = i, m(() => {
|
|
41
|
+
if (!g.current || a.current) return;
|
|
42
|
+
const e = new f.Map({
|
|
43
|
+
container: g.current,
|
|
44
|
+
style: S,
|
|
45
|
+
center: r,
|
|
46
|
+
zoom: c,
|
|
47
|
+
attributionControl: { compact: !0 }
|
|
48
|
+
});
|
|
49
|
+
return e.addControl(new f.NavigationControl({ showCompass: !1 })), e.on("load", () => {
|
|
50
|
+
d.current = !0, h(e, o);
|
|
51
|
+
}), e.on("click", (t) => {
|
|
52
|
+
var u;
|
|
53
|
+
(u = p.current) == null || u.call(p, { lng: t.lngLat.lng, lat: t.lngLat.lat });
|
|
54
|
+
}), a.current = e, () => {
|
|
55
|
+
e.remove(), a.current = null, d.current = !1;
|
|
56
|
+
};
|
|
57
|
+
}, []), m(() => {
|
|
58
|
+
const e = a.current;
|
|
59
|
+
e && (L.current.forEach((t) => t.remove()), L.current = n.map((t) => {
|
|
60
|
+
const u = new f.Marker({
|
|
61
|
+
color: t.color ?? R(t.estado, b)
|
|
62
|
+
}).setLngLat([t.lng, t.lat]);
|
|
63
|
+
return t.label && u.setPopup(new f.Popup({ offset: 24 }).setText(t.label)), u.addTo(e), u;
|
|
64
|
+
}));
|
|
65
|
+
}, [n, b]), m(() => {
|
|
66
|
+
const e = a.current;
|
|
67
|
+
!e || !d.current || h(e, o);
|
|
68
|
+
}, [o]), m(() => {
|
|
69
|
+
var t;
|
|
70
|
+
const e = a.current;
|
|
71
|
+
e && ((t = y.current) == null || t.remove(), y.current = null, l && (y.current = new f.Marker({ color: "#2563eb", scale: 1.2 }).setLngLat([l.lng, l.lat]).addTo(e)));
|
|
72
|
+
}, [l]), /* @__PURE__ */ j("div", { ref: g, style: { width: "100%", height: w } });
|
|
73
|
+
}
|
|
74
|
+
function h(r, c) {
|
|
75
|
+
for (const n of c) {
|
|
76
|
+
const o = `mn-zona-${n.id}`, i = n.geojson.type === "Feature" ? n.geojson : { type: "Feature", geometry: n.geojson, properties: {} }, l = r.getSource(o);
|
|
77
|
+
if (l) {
|
|
78
|
+
l.setData(i);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
r.addSource(o, { type: "geojson", data: i }), r.addLayer({
|
|
82
|
+
id: `${o}-fill`,
|
|
83
|
+
type: "fill",
|
|
84
|
+
source: o,
|
|
85
|
+
paint: { "fill-color": "#2563eb", "fill-opacity": 0.08 }
|
|
86
|
+
}), r.addLayer({
|
|
87
|
+
id: `${o}-line`,
|
|
88
|
+
type: "line",
|
|
89
|
+
source: o,
|
|
90
|
+
paint: { "line-color": "#2563eb", "line-width": 2, "line-dasharray": [2, 1] }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
O as COLORES_ESTADO,
|
|
96
|
+
M as ESTILO_OSM,
|
|
97
|
+
E as GeoMap,
|
|
98
|
+
R as colorPorEstado
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=mounajigeomap.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mounajigeomap.es.js","sources":["../src/estilo.ts","../src/GeoMap.tsx"],"sourcesContent":["import type { StyleSpecification } from 'maplibre-gl';\n\n/**\n * Estilo raster OSM sin API key. Para un pueblo alcanza y sobra; si un día\n * hace falta otro proveedor (Google, vector tiles), se cambia ACÁ y ninguna\n * app se entera — ese aislamiento es la razón de ser del paquete.\n * Tile usage policy de OSM: https://operations.osmfoundation.org/policies/tiles/\n */\nexport const ESTILO_OSM: StyleSpecification = {\n version: 8,\n sources: {\n osm: {\n type: 'raster',\n tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],\n tileSize: 256,\n attribution: '© OpenStreetMap contributors',\n },\n },\n layers: [{ id: 'osm', type: 'raster', source: 'osm' }],\n};\n\n/** Colores default por estado de pin; la app puede pasar `color` por pin. */\nexport const COLORES_ESTADO: Record<string, string> = {\n con_dueno: '#16a34a',\n callejero: '#ea580c',\n sin_verificar: '#6b7280',\n};\n\nexport function colorPorEstado(estado: string | undefined, overrides?: Record<string, string>): string {\n if (estado) {\n const color = overrides?.[estado] ?? COLORES_ESTADO[estado];\n if (color) return color;\n }\n return '#6b7280';\n}\n","import { useEffect, useRef } from 'react';\nimport maplibregl from 'maplibre-gl';\nimport type { Map as MlMap, StyleSpecification } from 'maplibre-gl';\nimport { ESTILO_OSM, colorPorEstado } from './estilo';\n\nexport interface GeoPin {\n id: string;\n lng: number;\n lat: number;\n /** Clave de estado (colorea vía colorPorEstado) o ignorada si viene `color`. */\n estado?: string;\n color?: string;\n /** Texto del popup al tocar el pin. */\n label?: string;\n}\n\nexport interface GeoZona {\n id: string;\n nombre?: string;\n /** Geometría o Feature GeoJSON del polígono. */\n geojson: GeoJSON.Geometry | GeoJSON.Feature;\n}\n\nexport interface GeoMapProps {\n centro: [lng: number, lat: number];\n zoom?: number;\n pins?: GeoPin[];\n zonas?: GeoZona[];\n /** Tap/click en el mapa (fuera de un pin). */\n onTap?: (punto: { lng: number; lat: number }) => void;\n /** Punto seleccionado — se muestra como marker destacado. */\n seleccionado?: { lng: number; lat: number } | null;\n alto?: string;\n estilo?: StyleSpecification | string;\n coloresEstado?: Record<string, string>;\n}\n\nexport function GeoMap({\n centro,\n zoom = 14,\n pins = [],\n zonas = [],\n onTap,\n seleccionado = null,\n alto = '70vh',\n estilo = ESTILO_OSM,\n coloresEstado,\n}: GeoMapProps) {\n const contenedorRef = useRef<HTMLDivElement | null>(null);\n const mapRef = useRef<MlMap | null>(null);\n const cargadoRef = useRef(false);\n const markersRef = useRef<maplibregl.Marker[]>([]);\n const seleccionadoRef = useRef<maplibregl.Marker | null>(null);\n const onTapRef = useRef(onTap);\n onTapRef.current = onTap;\n\n // Crear el mapa una sola vez.\n useEffect(() => {\n if (!contenedorRef.current || mapRef.current) return;\n const map = new maplibregl.Map({\n container: contenedorRef.current,\n style: estilo,\n center: centro,\n zoom,\n attributionControl: { compact: true },\n });\n map.addControl(new maplibregl.NavigationControl({ showCompass: false }));\n map.on('load', () => {\n cargadoRef.current = true;\n pintarZonas(map, zonas);\n });\n map.on('click', (e) => {\n onTapRef.current?.({ lng: e.lngLat.lng, lat: e.lngLat.lat });\n });\n mapRef.current = map;\n return () => {\n map.remove();\n mapRef.current = null;\n cargadoRef.current = false;\n };\n // El mapa no se recrea por cambios de centro/zoom: son solo el encuadre inicial.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Pins: pocos cientos en un censo de pueblo — rehacer markers es más simple\n // y suficiente que diffear.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return;\n markersRef.current.forEach((m) => m.remove());\n markersRef.current = pins.map((pin) => {\n const marker = new maplibregl.Marker({\n color: pin.color ?? colorPorEstado(pin.estado, coloresEstado),\n }).setLngLat([pin.lng, pin.lat]);\n if (pin.label) marker.setPopup(new maplibregl.Popup({ offset: 24 }).setText(pin.label));\n marker.addTo(map);\n return marker;\n });\n }, [pins, coloresEstado]);\n\n // Zonas (si cambian después del load).\n useEffect(() => {\n const map = mapRef.current;\n if (!map || !cargadoRef.current) return;\n pintarZonas(map, zonas);\n }, [zonas]);\n\n // Marker de selección.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return;\n seleccionadoRef.current?.remove();\n seleccionadoRef.current = null;\n if (seleccionado) {\n seleccionadoRef.current = new maplibregl.Marker({ color: '#2563eb', scale: 1.2 })\n .setLngLat([seleccionado.lng, seleccionado.lat])\n .addTo(map);\n }\n }, [seleccionado]);\n\n return <div ref={contenedorRef} style={{ width: '100%', height: alto }} />;\n}\n\nfunction pintarZonas(map: MlMap, zonas: GeoZona[]) {\n for (const zona of zonas) {\n const sourceId = `mn-zona-${zona.id}`;\n const data: GeoJSON.Feature =\n zona.geojson.type === 'Feature'\n ? zona.geojson\n : { type: 'Feature', geometry: zona.geojson as GeoJSON.Geometry, properties: {} };\n\n const existente = map.getSource(sourceId) as maplibregl.GeoJSONSource | undefined;\n if (existente) {\n existente.setData(data);\n continue;\n }\n map.addSource(sourceId, { type: 'geojson', data });\n map.addLayer({\n id: `${sourceId}-fill`,\n type: 'fill',\n source: sourceId,\n paint: { 'fill-color': '#2563eb', 'fill-opacity': 0.08 },\n });\n map.addLayer({\n id: `${sourceId}-line`,\n type: 'line',\n source: sourceId,\n paint: { 'line-color': '#2563eb', 'line-width': 2, 'line-dasharray': [2, 1] },\n });\n }\n}\n"],"names":["ESTILO_OSM","COLORES_ESTADO","colorPorEstado","estado","overrides","color","GeoMap","centro","zoom","pins","zonas","onTap","seleccionado","alto","estilo","coloresEstado","contenedorRef","useRef","mapRef","cargadoRef","markersRef","seleccionadoRef","onTapRef","useEffect","map","maplibregl","pintarZonas","e","_a","m","pin","marker","jsx","zona","sourceId","data","existente"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(n,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("react/jsx-runtime"),require("react"),require("maplibre-gl")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","maplibre-gl"],f):(n=typeof globalThis<"u"?globalThis:n||self,f(n.MounajiGeoMap={},n.jsxRuntime,n.React,n.maplibregl))})(this,(function(n,f,r,d){"use strict";const S={version:8,sources:{osm:{type:"raster",tiles:["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],tileSize:256,attribution:"© OpenStreetMap contributors"}},layers:[{id:"osm",type:"raster",source:"osm"}]},h={con_dueno:"#16a34a",callejero:"#ea580c",sin_verificar:"#6b7280"};function j(o,s){if(o){const u=(s==null?void 0:s[o])??h[o];if(u)return u}return"#6b7280"}function M({centro:o,zoom:s=14,pins:u=[],zonas:c=[],onTap:p,seleccionado:i=null,alto:b="70vh",estilo:w=S,coloresEstado:L}){const y=r.useRef(null),a=r.useRef(null),g=r.useRef(!1),O=r.useRef([]),R=r.useRef(null),m=r.useRef(p);return m.current=p,r.useEffect(()=>{if(!y.current||a.current)return;const e=new d.Map({container:y.current,style:w,center:o,zoom:s,attributionControl:{compact:!0}});return e.addControl(new d.NavigationControl({showCompass:!1})),e.on("load",()=>{g.current=!0,E(e,c)}),e.on("click",t=>{var l;(l=m.current)==null||l.call(m,{lng:t.lngLat.lng,lat:t.lngLat.lat})}),a.current=e,()=>{e.remove(),a.current=null,g.current=!1}},[]),r.useEffect(()=>{const e=a.current;e&&(O.current.forEach(t=>t.remove()),O.current=u.map(t=>{const l=new d.Marker({color:t.color??j(t.estado,L)}).setLngLat([t.lng,t.lat]);return t.label&&l.setPopup(new d.Popup({offset:24}).setText(t.label)),l.addTo(e),l}))},[u,L]),r.useEffect(()=>{const e=a.current;!e||!g.current||E(e,c)},[c]),r.useEffect(()=>{var t;const e=a.current;e&&((t=R.current)==null||t.remove(),R.current=null,i&&(R.current=new d.Marker({color:"#2563eb",scale:1.2}).setLngLat([i.lng,i.lat]).addTo(e)))},[i]),f.jsx("div",{ref:y,style:{width:"100%",height:b}})}function E(o,s){for(const u of s){const c=`mn-zona-${u.id}`,p=u.geojson.type==="Feature"?u.geojson:{type:"Feature",geometry:u.geojson,properties:{}},i=o.getSource(c);if(i){i.setData(p);continue}o.addSource(c,{type:"geojson",data:p}),o.addLayer({id:`${c}-fill`,type:"fill",source:c,paint:{"fill-color":"#2563eb","fill-opacity":.08}}),o.addLayer({id:`${c}-line`,type:"line",source:c,paint:{"line-color":"#2563eb","line-width":2,"line-dasharray":[2,1]}})}}n.COLORES_ESTADO=h,n.ESTILO_OSM=S,n.GeoMap=M,n.colorPorEstado=j,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
|
|
2
|
+
//# sourceMappingURL=mounajigeomap.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mounajigeomap.umd.cjs","sources":["../src/estilo.ts","../src/GeoMap.tsx"],"sourcesContent":["import type { StyleSpecification } from 'maplibre-gl';\n\n/**\n * Estilo raster OSM sin API key. Para un pueblo alcanza y sobra; si un día\n * hace falta otro proveedor (Google, vector tiles), se cambia ACÁ y ninguna\n * app se entera — ese aislamiento es la razón de ser del paquete.\n * Tile usage policy de OSM: https://operations.osmfoundation.org/policies/tiles/\n */\nexport const ESTILO_OSM: StyleSpecification = {\n version: 8,\n sources: {\n osm: {\n type: 'raster',\n tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],\n tileSize: 256,\n attribution: '© OpenStreetMap contributors',\n },\n },\n layers: [{ id: 'osm', type: 'raster', source: 'osm' }],\n};\n\n/** Colores default por estado de pin; la app puede pasar `color` por pin. */\nexport const COLORES_ESTADO: Record<string, string> = {\n con_dueno: '#16a34a',\n callejero: '#ea580c',\n sin_verificar: '#6b7280',\n};\n\nexport function colorPorEstado(estado: string | undefined, overrides?: Record<string, string>): string {\n if (estado) {\n const color = overrides?.[estado] ?? COLORES_ESTADO[estado];\n if (color) return color;\n }\n return '#6b7280';\n}\n","import { useEffect, useRef } from 'react';\nimport maplibregl from 'maplibre-gl';\nimport type { Map as MlMap, StyleSpecification } from 'maplibre-gl';\nimport { ESTILO_OSM, colorPorEstado } from './estilo';\n\nexport interface GeoPin {\n id: string;\n lng: number;\n lat: number;\n /** Clave de estado (colorea vía colorPorEstado) o ignorada si viene `color`. */\n estado?: string;\n color?: string;\n /** Texto del popup al tocar el pin. */\n label?: string;\n}\n\nexport interface GeoZona {\n id: string;\n nombre?: string;\n /** Geometría o Feature GeoJSON del polígono. */\n geojson: GeoJSON.Geometry | GeoJSON.Feature;\n}\n\nexport interface GeoMapProps {\n centro: [lng: number, lat: number];\n zoom?: number;\n pins?: GeoPin[];\n zonas?: GeoZona[];\n /** Tap/click en el mapa (fuera de un pin). */\n onTap?: (punto: { lng: number; lat: number }) => void;\n /** Punto seleccionado — se muestra como marker destacado. */\n seleccionado?: { lng: number; lat: number } | null;\n alto?: string;\n estilo?: StyleSpecification | string;\n coloresEstado?: Record<string, string>;\n}\n\nexport function GeoMap({\n centro,\n zoom = 14,\n pins = [],\n zonas = [],\n onTap,\n seleccionado = null,\n alto = '70vh',\n estilo = ESTILO_OSM,\n coloresEstado,\n}: GeoMapProps) {\n const contenedorRef = useRef<HTMLDivElement | null>(null);\n const mapRef = useRef<MlMap | null>(null);\n const cargadoRef = useRef(false);\n const markersRef = useRef<maplibregl.Marker[]>([]);\n const seleccionadoRef = useRef<maplibregl.Marker | null>(null);\n const onTapRef = useRef(onTap);\n onTapRef.current = onTap;\n\n // Crear el mapa una sola vez.\n useEffect(() => {\n if (!contenedorRef.current || mapRef.current) return;\n const map = new maplibregl.Map({\n container: contenedorRef.current,\n style: estilo,\n center: centro,\n zoom,\n attributionControl: { compact: true },\n });\n map.addControl(new maplibregl.NavigationControl({ showCompass: false }));\n map.on('load', () => {\n cargadoRef.current = true;\n pintarZonas(map, zonas);\n });\n map.on('click', (e) => {\n onTapRef.current?.({ lng: e.lngLat.lng, lat: e.lngLat.lat });\n });\n mapRef.current = map;\n return () => {\n map.remove();\n mapRef.current = null;\n cargadoRef.current = false;\n };\n // El mapa no se recrea por cambios de centro/zoom: son solo el encuadre inicial.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Pins: pocos cientos en un censo de pueblo — rehacer markers es más simple\n // y suficiente que diffear.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return;\n markersRef.current.forEach((m) => m.remove());\n markersRef.current = pins.map((pin) => {\n const marker = new maplibregl.Marker({\n color: pin.color ?? colorPorEstado(pin.estado, coloresEstado),\n }).setLngLat([pin.lng, pin.lat]);\n if (pin.label) marker.setPopup(new maplibregl.Popup({ offset: 24 }).setText(pin.label));\n marker.addTo(map);\n return marker;\n });\n }, [pins, coloresEstado]);\n\n // Zonas (si cambian después del load).\n useEffect(() => {\n const map = mapRef.current;\n if (!map || !cargadoRef.current) return;\n pintarZonas(map, zonas);\n }, [zonas]);\n\n // Marker de selección.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return;\n seleccionadoRef.current?.remove();\n seleccionadoRef.current = null;\n if (seleccionado) {\n seleccionadoRef.current = new maplibregl.Marker({ color: '#2563eb', scale: 1.2 })\n .setLngLat([seleccionado.lng, seleccionado.lat])\n .addTo(map);\n }\n }, [seleccionado]);\n\n return <div ref={contenedorRef} style={{ width: '100%', height: alto }} />;\n}\n\nfunction pintarZonas(map: MlMap, zonas: GeoZona[]) {\n for (const zona of zonas) {\n const sourceId = `mn-zona-${zona.id}`;\n const data: GeoJSON.Feature =\n zona.geojson.type === 'Feature'\n ? zona.geojson\n : { type: 'Feature', geometry: zona.geojson as GeoJSON.Geometry, properties: {} };\n\n const existente = map.getSource(sourceId) as maplibregl.GeoJSONSource | undefined;\n if (existente) {\n existente.setData(data);\n continue;\n }\n map.addSource(sourceId, { type: 'geojson', data });\n map.addLayer({\n id: `${sourceId}-fill`,\n type: 'fill',\n source: sourceId,\n paint: { 'fill-color': '#2563eb', 'fill-opacity': 0.08 },\n });\n map.addLayer({\n id: `${sourceId}-line`,\n type: 'line',\n source: sourceId,\n paint: { 'line-color': '#2563eb', 'line-width': 2, 'line-dasharray': [2, 1] },\n });\n }\n}\n"],"names":["ESTILO_OSM","COLORES_ESTADO","colorPorEstado","estado","overrides","color","GeoMap","centro","zoom","pins","zonas","onTap","seleccionado","alto","estilo","coloresEstado","contenedorRef","useRef","mapRef","cargadoRef","markersRef","seleccionadoRef","onTapRef","useEffect","map","maplibregl","pintarZonas","e","_a","m","pin","marker","jsx","zona","sourceId","data","existente"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StyleSpecification } from 'maplibre-gl';
|
|
2
|
+
export interface GeoPin {
|
|
3
|
+
id: string;
|
|
4
|
+
lng: number;
|
|
5
|
+
lat: number;
|
|
6
|
+
/** Clave de estado (colorea vía colorPorEstado) o ignorada si viene `color`. */
|
|
7
|
+
estado?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Texto del popup al tocar el pin. */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface GeoZona {
|
|
13
|
+
id: string;
|
|
14
|
+
nombre?: string;
|
|
15
|
+
/** Geometría o Feature GeoJSON del polígono. */
|
|
16
|
+
geojson: GeoJSON.Geometry | GeoJSON.Feature;
|
|
17
|
+
}
|
|
18
|
+
export interface GeoMapProps {
|
|
19
|
+
centro: [lng: number, lat: number];
|
|
20
|
+
zoom?: number;
|
|
21
|
+
pins?: GeoPin[];
|
|
22
|
+
zonas?: GeoZona[];
|
|
23
|
+
/** Tap/click en el mapa (fuera de un pin). */
|
|
24
|
+
onTap?: (punto: {
|
|
25
|
+
lng: number;
|
|
26
|
+
lat: number;
|
|
27
|
+
}) => void;
|
|
28
|
+
/** Punto seleccionado — se muestra como marker destacado. */
|
|
29
|
+
seleccionado?: {
|
|
30
|
+
lng: number;
|
|
31
|
+
lat: number;
|
|
32
|
+
} | null;
|
|
33
|
+
alto?: string;
|
|
34
|
+
estilo?: StyleSpecification | string;
|
|
35
|
+
coloresEstado?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export declare function GeoMap({ centro, zoom, pins, zonas, onTap, seleccionado, alto, estilo, coloresEstado, }: GeoMapProps): import("react").JSX.Element;
|
|
38
|
+
//# sourceMappingURL=GeoMap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeoMap.d.ts","sourceRoot":"","sources":["../../src/GeoMap.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGpE,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,YAAY,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,IAAS,EACT,IAAS,EACT,KAAU,EACV,KAAK,EACL,YAAmB,EACnB,IAAa,EACb,MAAmB,EACnB,aAAa,GACd,EAAE,WAAW,+BA0Eb"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StyleSpecification } from 'maplibre-gl';
|
|
2
|
+
/**
|
|
3
|
+
* Estilo raster OSM sin API key. Para un pueblo alcanza y sobra; si un día
|
|
4
|
+
* hace falta otro proveedor (Google, vector tiles), se cambia ACÁ y ninguna
|
|
5
|
+
* app se entera — ese aislamiento es la razón de ser del paquete.
|
|
6
|
+
* Tile usage policy de OSM: https://operations.osmfoundation.org/policies/tiles/
|
|
7
|
+
*/
|
|
8
|
+
export declare const ESTILO_OSM: StyleSpecification;
|
|
9
|
+
/** Colores default por estado de pin; la app puede pasar `color` por pin. */
|
|
10
|
+
export declare const COLORES_ESTADO: Record<string, string>;
|
|
11
|
+
export declare function colorPorEstado(estado: string | undefined, overrides?: Record<string, string>): string;
|
|
12
|
+
//# sourceMappingURL=estilo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estilo.d.ts","sourceRoot":"","sources":["../../src/estilo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,kBAWxB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAIjD,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMrG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mounaji_npm/geo-map",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin React map wrapper (MapLibre GL + OSM raster tiles by default): status-colored pins, GeoJSON zone layers, tap-to-mark and selected marker. Isolates the map provider so swapping it touches one package. Consumer must import maplibre-gl/dist/maplibre-gl.css.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/mounajigeomap.umd.cjs",
|
|
7
|
+
"module": "./dist/mounajigeomap.es.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"import": "./dist/mounajigeomap.es.js",
|
|
13
|
+
"require": "./dist/mounajigeomap.umd.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
22
|
+
"dev": "vite build --watch",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "npm run build && node --test \"tests/*.test.mjs\""
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mounaji",
|
|
28
|
+
"map",
|
|
29
|
+
"maplibre",
|
|
30
|
+
"osm",
|
|
31
|
+
"geojson",
|
|
32
|
+
"pins"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"maplibre-gl": "^4.7.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18",
|
|
40
|
+
"react-dom": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/GeoMap.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import maplibregl from 'maplibre-gl';
|
|
3
|
+
import type { Map as MlMap, StyleSpecification } from 'maplibre-gl';
|
|
4
|
+
import { ESTILO_OSM, colorPorEstado } from './estilo';
|
|
5
|
+
|
|
6
|
+
export interface GeoPin {
|
|
7
|
+
id: string;
|
|
8
|
+
lng: number;
|
|
9
|
+
lat: number;
|
|
10
|
+
/** Clave de estado (colorea vía colorPorEstado) o ignorada si viene `color`. */
|
|
11
|
+
estado?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Texto del popup al tocar el pin. */
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GeoZona {
|
|
18
|
+
id: string;
|
|
19
|
+
nombre?: string;
|
|
20
|
+
/** Geometría o Feature GeoJSON del polígono. */
|
|
21
|
+
geojson: GeoJSON.Geometry | GeoJSON.Feature;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GeoMapProps {
|
|
25
|
+
centro: [lng: number, lat: number];
|
|
26
|
+
zoom?: number;
|
|
27
|
+
pins?: GeoPin[];
|
|
28
|
+
zonas?: GeoZona[];
|
|
29
|
+
/** Tap/click en el mapa (fuera de un pin). */
|
|
30
|
+
onTap?: (punto: { lng: number; lat: number }) => void;
|
|
31
|
+
/** Punto seleccionado — se muestra como marker destacado. */
|
|
32
|
+
seleccionado?: { lng: number; lat: number } | null;
|
|
33
|
+
alto?: string;
|
|
34
|
+
estilo?: StyleSpecification | string;
|
|
35
|
+
coloresEstado?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function GeoMap({
|
|
39
|
+
centro,
|
|
40
|
+
zoom = 14,
|
|
41
|
+
pins = [],
|
|
42
|
+
zonas = [],
|
|
43
|
+
onTap,
|
|
44
|
+
seleccionado = null,
|
|
45
|
+
alto = '70vh',
|
|
46
|
+
estilo = ESTILO_OSM,
|
|
47
|
+
coloresEstado,
|
|
48
|
+
}: GeoMapProps) {
|
|
49
|
+
const contenedorRef = useRef<HTMLDivElement | null>(null);
|
|
50
|
+
const mapRef = useRef<MlMap | null>(null);
|
|
51
|
+
const cargadoRef = useRef(false);
|
|
52
|
+
const markersRef = useRef<maplibregl.Marker[]>([]);
|
|
53
|
+
const seleccionadoRef = useRef<maplibregl.Marker | null>(null);
|
|
54
|
+
const onTapRef = useRef(onTap);
|
|
55
|
+
onTapRef.current = onTap;
|
|
56
|
+
|
|
57
|
+
// Crear el mapa una sola vez.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!contenedorRef.current || mapRef.current) return;
|
|
60
|
+
const map = new maplibregl.Map({
|
|
61
|
+
container: contenedorRef.current,
|
|
62
|
+
style: estilo,
|
|
63
|
+
center: centro,
|
|
64
|
+
zoom,
|
|
65
|
+
attributionControl: { compact: true },
|
|
66
|
+
});
|
|
67
|
+
map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
|
|
68
|
+
map.on('load', () => {
|
|
69
|
+
cargadoRef.current = true;
|
|
70
|
+
pintarZonas(map, zonas);
|
|
71
|
+
});
|
|
72
|
+
map.on('click', (e) => {
|
|
73
|
+
onTapRef.current?.({ lng: e.lngLat.lng, lat: e.lngLat.lat });
|
|
74
|
+
});
|
|
75
|
+
mapRef.current = map;
|
|
76
|
+
return () => {
|
|
77
|
+
map.remove();
|
|
78
|
+
mapRef.current = null;
|
|
79
|
+
cargadoRef.current = false;
|
|
80
|
+
};
|
|
81
|
+
// El mapa no se recrea por cambios de centro/zoom: son solo el encuadre inicial.
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Pins: pocos cientos en un censo de pueblo — rehacer markers es más simple
|
|
86
|
+
// y suficiente que diffear.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const map = mapRef.current;
|
|
89
|
+
if (!map) return;
|
|
90
|
+
markersRef.current.forEach((m) => m.remove());
|
|
91
|
+
markersRef.current = pins.map((pin) => {
|
|
92
|
+
const marker = new maplibregl.Marker({
|
|
93
|
+
color: pin.color ?? colorPorEstado(pin.estado, coloresEstado),
|
|
94
|
+
}).setLngLat([pin.lng, pin.lat]);
|
|
95
|
+
if (pin.label) marker.setPopup(new maplibregl.Popup({ offset: 24 }).setText(pin.label));
|
|
96
|
+
marker.addTo(map);
|
|
97
|
+
return marker;
|
|
98
|
+
});
|
|
99
|
+
}, [pins, coloresEstado]);
|
|
100
|
+
|
|
101
|
+
// Zonas (si cambian después del load).
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const map = mapRef.current;
|
|
104
|
+
if (!map || !cargadoRef.current) return;
|
|
105
|
+
pintarZonas(map, zonas);
|
|
106
|
+
}, [zonas]);
|
|
107
|
+
|
|
108
|
+
// Marker de selección.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const map = mapRef.current;
|
|
111
|
+
if (!map) return;
|
|
112
|
+
seleccionadoRef.current?.remove();
|
|
113
|
+
seleccionadoRef.current = null;
|
|
114
|
+
if (seleccionado) {
|
|
115
|
+
seleccionadoRef.current = new maplibregl.Marker({ color: '#2563eb', scale: 1.2 })
|
|
116
|
+
.setLngLat([seleccionado.lng, seleccionado.lat])
|
|
117
|
+
.addTo(map);
|
|
118
|
+
}
|
|
119
|
+
}, [seleccionado]);
|
|
120
|
+
|
|
121
|
+
return <div ref={contenedorRef} style={{ width: '100%', height: alto }} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pintarZonas(map: MlMap, zonas: GeoZona[]) {
|
|
125
|
+
for (const zona of zonas) {
|
|
126
|
+
const sourceId = `mn-zona-${zona.id}`;
|
|
127
|
+
const data: GeoJSON.Feature =
|
|
128
|
+
zona.geojson.type === 'Feature'
|
|
129
|
+
? zona.geojson
|
|
130
|
+
: { type: 'Feature', geometry: zona.geojson as GeoJSON.Geometry, properties: {} };
|
|
131
|
+
|
|
132
|
+
const existente = map.getSource(sourceId) as maplibregl.GeoJSONSource | undefined;
|
|
133
|
+
if (existente) {
|
|
134
|
+
existente.setData(data);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
map.addSource(sourceId, { type: 'geojson', data });
|
|
138
|
+
map.addLayer({
|
|
139
|
+
id: `${sourceId}-fill`,
|
|
140
|
+
type: 'fill',
|
|
141
|
+
source: sourceId,
|
|
142
|
+
paint: { 'fill-color': '#2563eb', 'fill-opacity': 0.08 },
|
|
143
|
+
});
|
|
144
|
+
map.addLayer({
|
|
145
|
+
id: `${sourceId}-line`,
|
|
146
|
+
type: 'line',
|
|
147
|
+
source: sourceId,
|
|
148
|
+
paint: { 'line-color': '#2563eb', 'line-width': 2, 'line-dasharray': [2, 1] },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/estilo.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StyleSpecification } from 'maplibre-gl';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Estilo raster OSM sin API key. Para un pueblo alcanza y sobra; si un día
|
|
5
|
+
* hace falta otro proveedor (Google, vector tiles), se cambia ACÁ y ninguna
|
|
6
|
+
* app se entera — ese aislamiento es la razón de ser del paquete.
|
|
7
|
+
* Tile usage policy de OSM: https://operations.osmfoundation.org/policies/tiles/
|
|
8
|
+
*/
|
|
9
|
+
export const ESTILO_OSM: StyleSpecification = {
|
|
10
|
+
version: 8,
|
|
11
|
+
sources: {
|
|
12
|
+
osm: {
|
|
13
|
+
type: 'raster',
|
|
14
|
+
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
15
|
+
tileSize: 256,
|
|
16
|
+
attribution: '© OpenStreetMap contributors',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Colores default por estado de pin; la app puede pasar `color` por pin. */
|
|
23
|
+
export const COLORES_ESTADO: Record<string, string> = {
|
|
24
|
+
con_dueno: '#16a34a',
|
|
25
|
+
callejero: '#ea580c',
|
|
26
|
+
sin_verificar: '#6b7280',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function colorPorEstado(estado: string | undefined, overrides?: Record<string, string>): string {
|
|
30
|
+
if (estado) {
|
|
31
|
+
const color = overrides?.[estado] ?? COLORES_ESTADO[estado];
|
|
32
|
+
if (color) return color;
|
|
33
|
+
}
|
|
34
|
+
return '#6b7280';
|
|
35
|
+
}
|
package/src/index.ts
ADDED