npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@open-urbis/map

v1.0.0

Published

Pacote de componentes de mapa React focados em sistemas urbanos e interações espaciais (GIS) para o ecossistema Urbis.

Downloads

133

Readme

@open-urbis/map

Pacote de componentes de mapa React focados em sistemas urbanos e interações espaciais (GIS) para o ecossistema Urbis.

Instalação

Você pode instalar o pacote diretamente através do registro público do NPM.

# via pnpm
pnpm add @open-urbis/map

# via npm
npm install @open-urbis/map

# via yarn
yarn add @open-urbis/map

Nota: Certifique-se de configurar e carregar adequadamente suas variáveis de ambiente, como por exemplo os tokens do Mapbox e a base URL da API, em sua aplicação host (por exemplo via .env), caso utilize as funcionalidades que batem em mapas base ou APIs.


Componentes e Padrões Principais

O pacote disponibiliza duas abordagens primárias de UI voltadas para casos de uso diferentes dentro da Prefeitura/Urbis:

  1. MapPicker: Focado na seleção de áreas no mapa e visualização/edição de perímetros (usado fortemente em processos de licenciamento, "Ficha do Imóvel").
  2. DynamicSystem: Um ecossistema de componentes para visualização mais robusta de buscas dinâmicas e exploração de dados no mapa.

Abaixo, detalhamos como implementá-los seguindo as melhores práticas adotadas na plataforma.


1. MapPicker e Edição de Perímetros

O MapPicker é projetado para atuar como um campo de formulário super avançado, onde o usuário pode interagir, visualizar lotes ou editar áreas para processos.

A sua implementação em um sistema completo exige a associação a um componente de Overlay (para visualização de resultados em tela cheia) utilizando os hooks de contexto providos pelo próprio @open-urbis/map (usePolygonEditContext, useMapContext).

Exemplo prático de um "MapTest" integrado

import { MapPicker, FeaturesView, PolygonDetails, usePolygonEditContext, useMapContext } from "@open-urbis/map";
import { Button } from "@/components/ui/button";
import { useState, useCallback, useMemo } from "react";
import { MapActionToolbar } from "./MapActionToolbar";

const EXAMPLE_DATA = {
    editFeature: {
        type: "Feature",
        properties: {},
        geometry: {
            type: "Polygon",
            coordinates: [
                [
                    [-46.656551, -23.561573],
                    [-46.656551, -23.562573],
                    [-46.655551, -23.562573],
                    [-46.655551, -23.561573],
                    [-46.656551, -23.561573]
                ]
            ]
        }
    }
};

const MapResultContent = ({ value, onClose, hideActions = false }: { value: any, onClose: () => void, hideActions?: boolean }) => {
    const { editFeatureTemplate, layerWithRootEditTemplate, editFeature: startEditing } = usePolygonEditContext();
    const { layerSchemas } = useMapContext();

    const rootTemplate = useMemo(() => {
        if (!layerWithRootEditTemplate.value) return [];
        const layer = layerSchemas.value.find(l => l.id === layerWithRootEditTemplate.value);
        return layer?.viewTemplate || [];
    }, [layerSchemas.value, layerWithRootEditTemplate.value]);

    const handleEdit = () => {
        if (!value) return;
        const featureToEdit = value.type === 'selection' ? value.features?.[0]?.feature : value.editFeature;
        if (featureToEdit) {
            startEditing(featureToEdit);
            onClose(); 
        }
    };

    return (
        <div className="absolute inset-0 bg-background flex flex-col animate-in fade-in duration-300 pointer-events-auto">
            <div className="w-full h-full flex flex-col p-6">
                <div>
                    <h2 className="text-2xl font-bold tracking-tight">Ficha do imovel</h2>
                    <p className="text-sm text-muted-foreground mb-2">Verifique os dados abaixo para prosseguir com o protocolo.</p>
                </div>

                {!hideActions && (
                    <MapActionToolbar
                        onChangeSelection={value.type === 'edit' || value.type === 'digital' ? onClose : undefined}
                        onEdit={value.type === 'selection' ? handleEdit : undefined}
                        onPrint={() => window.print()}
                        className="mt-4"
                    />
                )}

                <div className="flex-1 overflow-y-auto bg-card border rounded-xl shadow-sm">
                    <div className="p-6">
                        {!value ? (
                            <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                                <span className="material-symbols-outlined text-4xl mb-4 opacity-50">data_object</span>
                                <p className="font-medium">Nenhum dado selecionado</p>
                            </div>
                        ) : value.editFeature && value.loading ? (
                            <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                                <span className="material-symbols-outlined text-4xl mb-4 animate-spin">progress_activity</span>
                                <p className="font-medium">Carregando dados do imovel...</p>
                            </div>
                        ) : value.editFeature && editFeatureTemplate.value ? (
                            <PolygonDetails
                                template={editFeatureTemplate.value}
                                rootTemplate={rootTemplate}
                            />
                        ) : value.editFeature && !editFeatureTemplate.value ? (
                             <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                                <span className="material-symbols-outlined text-4xl mb-4 animate-spin">progress_activity</span>
                                <p className="font-medium">Carregando formulário...</p>
                            </div>
                        ) : value.type === 'selection' && value.features?.[0] ? (
                            <div className="space-y-4">
                                <h3 className="text-lg font-semibold">Dados da Seleção</h3>
                                <FeaturesView feature={value.features[0]} />
                            </div>
                        ) : (
                            <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                                <span className="material-symbols-outlined text-4xl mb-4 opacity-50">data_object</span>
                                <p className="font-medium">Dados brutos do perímetro</p>
                                <pre className="mt-4 p-4 bg-muted rounded-lg text-xs font-mono text-left w-full max-w-2xl overflow-auto">
                                    {JSON.stringify(value, null, 2)}
                                </pre>
                            </div>
                        )}
                    </div>
                </div>

                {!hideActions && (
                    <div className="mt-6 flex justify-end items-center gap-4">
                        <span className="text-sm text-muted-foreground">
                            Ao protocolar, este perímetro será vinculado ao seu processo.
                        </span>
                        <Button
                            onClick={() => {
                                console.log("Confirmar e Protocolar action for value:", value);
                                alert("Protocolo iniciado com sucesso!");
                            }}
                            size="lg"
                            className="px-8 shadow-lg"
                        >
                            Confirmar e Protocolar
                        </Button>
                    </div>
                )}
            </div>
        </div>
    );
};

const MapTestPage = () => {
    const [value, setValue] = useState<any>(null);
    const [showResult, setShowResult] = useState(false);
    const [initialData, setInitialData] = useState<any>();
    const [pickerKey, setPickerKey] = useState(0);
    const [lastIntersections, setLastIntersections] = useState<any>(null);
    const [lastSelectionId, setLastSelectionId] = useState<string | null>(null);

    const handleMapChange = useCallback((v: any) => {
        setValue(v);
        
        if (v?.intersections && v.intersections !== lastIntersections) {
            setLastIntersections(v.intersections);
            setShowResult(true);
        }
        
        if ((v?.type === 'edit' || v?.type === 'view') && v?.editFeature && !v.intersections && v.intersections !== lastIntersections) {
            setShowResult(true);
        }

        if (v?.type === 'selection') {
            const currentSelection = v?.features?.[0];
            const currentSelectionKey = currentSelection 
                ? (currentSelection.id || JSON.stringify(currentSelection.properties) || JSON.stringify(currentSelection.geometry))
                : null;

            if (currentSelectionKey && currentSelectionKey !== lastSelectionId) {
                setLastSelectionId(currentSelectionKey);
                setShowResult(true);
            }
        }
    }, [lastIntersections, lastSelectionId]);

    return (
        <div className="rounded-xl w-[100%] h-[600px] border shadow-sm bg-background relative flex flex-col overflow-hidden">
            <div className="flex-1 relative overflow-hidden">
                <MapPicker
                    key={pickerKey}
                    initialData={initialData}
                    onChange={handleMapChange}
                    mode={initialData !== undefined ? 'selected' : 'editable'}
                    hideMap={showResult}
                    hideLayerManager={true}
                    overlay={
                         (showResult) && (
                            <MapResultContent 
                                value={value} 
                                onClose={() => {
                                    setShowResult(false);
                                    setLastSelectionId(null);
                                }} 
                                hideActions={!!initialData}
                            />
                        )
                    }
                >
                </MapPicker>
            </div>
        </div>
    );
};

export default MapTestPage;

2. DynamicSystem (Interface de Exploração e Buscas)

Para criar painéis complexos de exploração baseada em mapas, onde há uma barra lateral controlando buscas dinâmicas (Search Filters) e uma área inferior para exibição das listagens, utilizamos o ecossistema DynamicSystem.

Esses componentes devem ser envelopados pelo DynamicSystemProvider.

Exemplo Prático de Integração do Mapa Dinâmico

import { useState } from 'react';
import { 
  DynamicSystemProvider, 
  DynamicSystemSidebar, 
  DynamicSystemResults 
} from '@open-urbis/map';
import { Minus, Maximize2, X, FileText } from 'lucide-react';

export function MapDataTestPage() {
  const [isResultsOpen, setIsResultsOpen] = useState(true);
  const [isResultsMinimized, setIsResultsMinimized] = useState(false);

  return (
    <div className="fixed inset-0 w-screen h-screen overflow-hidden bg-slate-50">
      {/* 1. Deve envelopar a hierarquia para compartilhar estado do sistema */}
      <DynamicSystemProvider>
        <div className="flex flex-col md:flex-row h-full w-full">
          
          {/* 2. Sidebar Esquerda de Controles */}
          <div className="md:h-full z-20" style={{maxWidth: "420px"}}>
            <DynamicSystemSidebar />
          </div>
          
          {/* Área Principal (onde o Mapa é instanciado na aplicação) */}
          <div className="flex-1 relative flex items-center justify-center border-l">
            <p>[ Espaço do Mapa ]</p>
            
            {/* 3. Janela de Resultados Flutuante da Tabela */}
            {isResultsOpen && (
              <div 
                className={`absolute bottom-0 right-6 bg-white rounded-t-2xl shadow-2xl border z-50 flex flex-col transition-all duration-300 ${
                  isResultsMinimized ? 'w-[320px] h-[48px]' : 'w-[800px] h-[600px] max-h-[85vh]'
                }`}
              >
                {/* Header Customizado pelo Usuário na app consumidora */}
                <div 
                  className="px-4 py-3 flex justify-between items-center cursor-pointer border-b"
                  onClick={() => setIsResultsMinimized(!isResultsMinimized)}
                >
                  <h3 className="text-sm font-semibold flex items-center gap-2"><FileText size={16}/> Resultados</h3>
                  <div className="flex gap-3">
                    <button onClick={(e) => { e.stopPropagation(); setIsResultsMinimized(!isResultsMinimized); }}>
                      {isResultsMinimized ? <Maximize2 size={16}/> : <Minus size={16}/>}
                    </button>
                    <button onClick={(e) => { e.stopPropagation(); setIsResultsOpen(false); }}>
                      <X size={16} />
                    </button>
                  </div>
                </div>

                {/* Área de Inserção da Tabela Resultante */}
                <div className={`flex-1 overflow-hidden transition-opacity ${isResultsMinimized ? 'opacity-0 hidden' : 'opacity-100'}`}>
                  <DynamicSystemResults />
                </div>
              </div>
            )}
            
          </div>
        </div>
      </DynamicSystemProvider>
    </div>
  );
}

Nesse padrão, a aplicação consumidora (o app hospedeiro) detém o controle físico (tamanho de janelas, minimização, posição na tela) e delega as renderizações pesadas (Sidebar e Tabela) para os componentes DynamicSystemSidebar e DynamicSystemResults.


3. Validação e Estruturação de Projetos (MapDataIntegrationField)

O MapDataIntegrationField é um visualizador avançado e validador de estruturas complexas de dados geográficos (tipicamente JSONs contendo extrações complexas de DWG ou BIM). Ele combina um MapPreview 3D lado-a-lado com um visor estruturado (Tree View) dos parâmetros lidos, permitindo inspeção granular e visual das geometrias.

Exemplo prático de Validação de DWG

import { useState } from "react";
import { MapDataIntegrationField } from "@open-urbis/map";
import { Button } from "@/components/ui/button";

// Exemplo JSON incluído dentro de '@open-urbis/map/examples/MapDataIntegrationExample'
import { EXAMPLE_DATA } from "@open-urbis/map/examples/MapDataIntegrationExample";

export const MapDataIntegrationTestPage = () => {
  const [value, setValue] = useState<any>(null);
  const [initialData, setInitialData] = useState<any>();
  const [mode, setMode] = useState<'edit' | 'view'>('edit');
  const [fieldKey, setFieldKey] = useState(0);

  const loadExample = async () => {
    setInitialData(EXAMPLE_DATA);
    setMode('view');
    setFieldKey(k => k + 1);
    setValue(EXAMPLE_DATA);
  };

  const handleDataChange = (data: any) => {
    setValue(data); // Disparado após parsing/edições de JSON customizado
  };

  return (
    <div className="w-full h-full p-8 flex flex-col gap-4">
        <div className="flex items-center justify-between">
            <div>
              <h2 className="text-2xl font-bold">Validação de Projeto</h2>
              <p>Envie um JSON DWG/BIM para visualizar as propriedades em 3D.</p>
            </div>
            <Button onClick={loadExample}>Ver Exemplo 3D</Button>
        </div>

        <div className="bg-white border rounded-lg p-6 min-h-[600px]">
          <MapDataIntegrationField
            key={fieldKey}
            mode={mode}
            initialData={initialData}
            onChange={handleDataChange}
          />
        </div>
    </div>
  );
};

Estrutura do MapPickerValue

Quando o MapPicker dispara a função onChange(value), ele envia as informações essenciais para a sua aplicação lidar com o dado e persistí-lo ou enviar a APIs:

interface MapPickerValue {
  type: 'selection' | 'edit' | 'view' | 'digital';
  
  // Features selecionadas via clique ou busca (no modo view/selection)
  selectedFeatures?: MapContextSelectedFeature[];
  
  // Feature em processo de criação/edição no modo 'editable'
  editFeature?: Feature | null;
  
  // Intersecções cruzadas com o polígono desenhado
  intersections?: any;

  // Em modo 'digital' retorna a feature da busca do endereço
  digitalAddress?: FeatureCollection | null;
}

Consumo de Contextos Internos

O @open-urbis/map gerencia estados intensamente usando a biblioteca de Signals do Preact. Se você for acessar dados via usePolygonEditContext() ou useMapContext(), certifique-se de referenciar o valor com .value.

const { loading, data } = usePolygonEditContext();

// Correto (usar em if/render)
if (loading.value) { return <Spinner /> }

// Incorreto 
if (loading) { return <Spinner /> } // loading é um objeto Signal