@lucabeapps/react-network-map
v0.2.0
Published
Componente React para exibir redes elétricas (postes, rede BT/MT, transformadores...) sobre OpenStreetMap com MapLibre GL. Busca dados por bounding box e é totalmente customizável.
Maintainers
Readme
@lucabeapps/react-network-map
Componente React para exibir redes elétricas (postes, rede BT/MT, transformadores, chaves, iluminação…) sobre OpenStreetMap, usando MapLibre GL. Busca as features por bounding box conforme o usuário navega e é totalmente customizável.
- 🗺️ Base OpenStreetMap (ou qualquer tile/estilo MapLibre)
- ⚡ WebGL — aguenta milhares de pontos/linhas, com clustering opcional
- 🔌 Fonte de dados plugável (sua API por bbox, com adapter pronto)
- 🎨 Estilos por camada, controle de liga/desliga, popups customizáveis
- 📦 TypeScript, ESM + CJS, tree-shakeable
Instalação
npm install @lucabeapps/react-network-map maplibre-gl react react-dommaplibre-gl, react e react-dom são peer dependencies.
Importe o CSS do MapLibre uma vez no seu app:
import "maplibre-gl/dist/maplibre-gl.css";Uso rápido
import { NetworkMap, createBboxProvider } from "@polestrack/react-network-map";
import "maplibre-gl/dist/maplibre-gl.css";
const provider = createBboxProvider({
url: "https://poles.lucabeapps.com.br/features", // sua API (polestrack-api)
schema: "enel_sp_2024",
});
export default function App() {
return (
<div style={{ height: "100vh" }}>
<NetworkMap
provider={provider}
initialViewState={{ longitude: -46.633, latitude: -23.55, zoom: 15 }}
minZoom={13}
onFeatureClick={(f) => console.log(f.properties)}
/>
</div>
);
}O contêiner precisa ter altura definida (ex.:
height: 100vhouflex: 1).
Sem escolher a distribuidora (auto-descoberta)
Se você não quer que o usuário selecione a distribuidora, use autoLocate: o
mapa descobre sozinho qual concessão cobre a área conforme navega (e re-descobre
ao entrar noutra). Basta não passar schema:
const provider = createBboxProvider({
url: "https://poles.lucabeapps.com.br/features",
autoLocate: true, // acha a distribuidora sozinho
onLocate: (schema) => console.log("distribuidora:", schema), // opcional
});Requer o endpoint GET {base}/locate?west&south&east&north → { "schemas": [...] }
(o polestrack-api já expõe). Por padrão a URL é derivada trocando /features
por /locate; se sua rota for outra, passe locateUrl.
Nota: a biblioteca é só o mapa (visualização). Cadastro de postes, análise por câmera (AR) e importação de KML são recursos do app que consome a lib, não fazem parte do pacote.
De onde vêm os dados
A lib chama o provider sempre que o mapa é movido (acima de minZoom), passando a
bounding box atual. O padrão é createBboxProvider, que faz um GET no seu endpoint:
GET {url}?west=..&south=..&east=..&north=..&zoom=..&schema=..&layers=postes,rede_bt,..e espera um GeoJSON FeatureCollection cujas features tenham properties._layer
(mesmo formato do conectagd/do banco gerado pelo scraper mapear-brasil). Um servidor
de referência pronto acompanha o projeto mapear-brasil (api-server.mjs).
Se o formato do seu backend for diferente, use transform:
createBboxProvider({ url, transform: (raw) => ({ type: "FeatureCollection", features: raw.dados }) });CORS / cookies (importante)
Por padrão o fetch usa credentials: "same-origin", que funciona com APIs
públicas que respondem Access-Control-Allow-Origin: * (o caso da polestrack-api).
⚠️ Não combine credentials: "include" com Allow-Origin: * — o navegador
bloqueia a requisição silenciosamente (o mapa fica vazio). Só use
createBboxProvider({ ..., credentials: "include" }) se sua API precisa de cookie
cross-site — e aí ela precisa refletir a origem exata no CORS +
Access-Control-Allow-Credentials: true (não pode usar *).
Garanta também que o CORS_ORIGIN da API inclua o domínio do seu front.
Ou passe sua própria função direto, sem adapter:
<NetworkMap fetchFeatures={async ({ bbox, zoom, layers, signal }) => {
const r = await fetch(`/api?…`, { signal });
return r.json(); // FeatureCollection com properties._layer
}} />Customização
Estilos por camada
Os estilos do usuário estendem (não substituem) os padrões de cada camada:
<NetworkMap
provider={provider}
layers={["postes", "rede_bt", "rede_mt", "transformadores"]} // só estas
layerStyles={{
postes: { color: "#00e5ff", radius: 4 },
rede_mt: { color: "#ff3300", width: 3 },
transformadores: { color: "#ffcc00", radius: 6, strokeColor: "#000", strokeWidth: 1 },
}}
/>Campos de LayerStyle: label, geometry (auto|point|line|polygon), color,
radius, width, opacity, strokeColor, strokeWidth, visible, minzoom,
maxzoom, e overrides brutos do MapLibre: circlePaint, linePaint, fillPaint.
Seletor de camadas por fora (sua UI)
O painel de camadas interno é um controle do mapa (bom em tela cheia, mas pode
estourar dentro de um card pequeno). Para montar o seu seletor por fora,
esconda o interno com showLayerToggle={false} e use onLayers — ele entrega a
lista de camadas (rótulo, cor, forma do ícone, visível, contagem ao vivo) e um
setVisible para ligar/desligar:
const [layers, setLayers] = useState<LayerInfo[]>([]);
const apiRef = useRef<LayersApi>();
<NetworkMap
provider={provider}
showLayerToggle={false}
onLayers={(api) => { apiRef.current = api; setLayers(api.layers); }}
/>
{/* seu painel, onde você quiser */}
{layers.map((l) => (
<label key={l.key}>
<input type="checkbox" checked={l.visible}
onChange={(e) => apiRef.current!.setVisible(l.key, e.target.checked)} />
{l.label} {l.count != null && `(${l.count})`}
</label>
))}Os pontos são desenhados no mapa com o ícone da forma de cada camada (triângulo p/ transformador, losango, hexágono, "chave"…), iguais aos da legenda.
Clustering (agrupar pontos densos)
<NetworkMap provider={provider} cluster clusterRadius={60} />Afeta só camadas de ponto; linhas continuam normais. Clicar num cluster dá zoom.
Popup customizado
<NetworkMap
provider={provider}
renderPopup={(f) => `<b>${f.properties._layer}</b><br/>ID: ${f.properties.cod_id}`}
/>Retorne string, um HTMLElement, ou null para não abrir popup. Para painéis em
React, use onFeatureClick e controle o estado no seu app.
Base map / tiles
<NetworkMap osmTileUrl="https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
// ou um estilo MapLibre completo (vetorial, satélite, etc.):
<NetworkMap mapStyle="https://demotiles.maplibre.org/style.json" />Acesso ao mapa (escape hatch)
<NetworkMap onLoad={(map) => { /* objeto maplibregl.Map — faça o que quiser */ }} />Props
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| provider | DataProvider | — | Fonte de dados (use createBboxProvider) |
| fetchFeatures | (ctx) => Promise<FeatureCollection> | — | Alternativa ao provider |
| layers | string[] | todas | Quais camadas renderizar |
| layerStyles | Record<string, LayerStyle> | padrões | Estende os estilos por camada |
| initialViewState | {longitude,latitude,zoom} | SP | View inicial |
| minZoom | number | 13 | Abaixo disso não busca dados |
| cluster | boolean | false | Agrupa pontos |
| clusterRadius | number | 50 | Raio do cluster (px) |
| osmTileUrl | string | OSM | URL dos tiles raster |
| mapStyle | StyleSpecification\|string | — | Override total do estilo base |
| debounceMs | number | 300 | Debounce das buscas ao mover |
| showControls | boolean | true | Zoom/bússola/escala |
| showLayerToggle | boolean | true | Painel liga/desliga camadas |
| renderPopup | (f) => string\|HTMLElement\|null | tabela auto | Conteúdo do popup |
| onFeatureClick | (f, ev) => void | — | Clique em feature |
| onViewStateChange | (v) => void | — | Ao mover o mapa |
| onError | (err) => void | — | Erro de busca |
| onLoad | (map) => void | — | Acesso ao maplibregl.Map |
| className / style | | | No contêiner |
Exports
NetworkMap, createBboxProvider, conectagdProvider, DEFAULT_LAYER_STYLES,
DEFAULT_LAYER_ORDER, LayerToggleControl, e os tipos
(NetworkMapProps, LayerStyle, DataProvider, NetworkFeature, …).
Desenvolvimento
npm install
npm run build # gera dist/ (ESM + CJS + tipos)
npm run typecheck
cd example && npm install && npm run dev # demo com dados mockadosPublicar no npm
npm login
npm publish --access public # escopo @polestrack precisa de --access publicLicença
MIT
