splaz 0.1.0__tar.gz
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.
- splaz-0.1.0/LICENSE +21 -0
- splaz-0.1.0/PKG-INFO +12 -0
- splaz-0.1.0/README.md +111 -0
- splaz-0.1.0/pyproject.toml +21 -0
- splaz-0.1.0/setup.cfg +4 -0
- splaz-0.1.0/src/splaz/__init__.py +17 -0
- splaz-0.1.0/src/splaz/constants.py +18 -0
- splaz-0.1.0/src/splaz/downloader.py +105 -0
- splaz-0.1.0/src/splaz/entities.py +50 -0
- splaz-0.1.0/src/splaz/geocoder.py +90 -0
- splaz-0.1.0/src/splaz.egg-info/PKG-INFO +12 -0
- splaz-0.1.0/src/splaz.egg-info/SOURCES.txt +17 -0
- splaz-0.1.0/src/splaz.egg-info/dependency_links.txt +1 -0
- splaz-0.1.0/src/splaz.egg-info/requires.txt +5 -0
- splaz-0.1.0/src/splaz.egg-info/top_level.txt +1 -0
- splaz-0.1.0/tests/test_downloader.py +52 -0
- splaz-0.1.0/tests/test_end2end.py +34 -0
- splaz-0.1.0/tests/test_entities.py +37 -0
- splaz-0.1.0/tests/test_geocoder.py +73 -0
splaz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João Braga
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
splaz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splaz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API para facilitar o acesso a dados LiDAR do GeoSampa
|
|
5
|
+
Author: João Braga
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: requests
|
|
8
|
+
Requires-Dist: geopandas
|
|
9
|
+
Requires-Dist: geopy
|
|
10
|
+
Requires-Dist: shapely
|
|
11
|
+
Requires-Dist: laspy
|
|
12
|
+
Dynamic: license-file
|
splaz-0.1.0/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SpLaz (Vagas Verdes)
|
|
2
|
+
A SpLaz é uma biblioteca Python de alto nível projetada para automatizar a obtenção de dados LiDAR (nuvens de pontos) da cidade de São Paulo via portal GeoSampa. Ela resolve complexidades de geocodificação, conversão de sistemas de coordenadas (SIRGAS 2000 / UTM 23S) e instabilidades de download de servidores legados.
|
|
3
|
+
|
|
4
|
+
Este projeto é a base técnica do projeto Vagas Verdes, focado em identificar áreas potenciais para arborização urbana utilizando sensoriamento remoto.
|
|
5
|
+
|
|
6
|
+
## 🚀 Funcionalidades
|
|
7
|
+
- Geocodificação por Endereço: Identifica automaticamente qual quadrante LiDAR baixar a partir de um endereço textual (ex: "Rua Quatá, 300").
|
|
8
|
+
|
|
9
|
+
- Busca por Bairro: Lista e baixa todos os quadrantes que compõem um bairro específico.
|
|
10
|
+
|
|
11
|
+
- Tratamento de Dados Espaciais: Download e extração automática da grade de articulação (Shapefile) da prefeitura.
|
|
12
|
+
|
|
13
|
+
- Resiliência de Rede: Sistema de correção de encoding (ISO-8859-1) e verificação de integridade de arquivos ZIP.
|
|
14
|
+
|
|
15
|
+
- Gestão de Cache: Armazenamento inteligente na pasta local .geosp_laz_api para evitar downloads repetitivos e poupar banda.
|
|
16
|
+
|
|
17
|
+
## 📦 Instalação
|
|
18
|
+
O próximo passo do projeto será a publicação no PyPI. Atualmente, a biblioteca pode ser instalada diretamente via pip a partir do diretório raiz:
|
|
19
|
+
|
|
20
|
+
Bash
|
|
21
|
+
|
|
22
|
+
### Instalação em modo editável (ideal para desenvolvimento)
|
|
23
|
+
pip install -e .
|
|
24
|
+
### 🛠 Exemplos de Uso
|
|
25
|
+
#### 1. Configuração Inicial
|
|
26
|
+
```Python
|
|
27
|
+
|
|
28
|
+
from splaz import GeospLidarClient, GeospGeocoder
|
|
29
|
+
|
|
30
|
+
client = SpLaz()
|
|
31
|
+
geo = SpLazGeo(client=client)
|
|
32
|
+
```
|
|
33
|
+
#### 2. Download por Código do Quadrante
|
|
34
|
+
Ideal quando você já possui o mapeamento da grade de 2020:
|
|
35
|
+
|
|
36
|
+
```Python
|
|
37
|
+
|
|
38
|
+
quadrante = client.download_quadrante("3316-153")
|
|
39
|
+
quadrante.save("data/raw/laz")
|
|
40
|
+
```
|
|
41
|
+
#### 3. Download por Endereço (Geocodificação)
|
|
42
|
+
A forma mais intuitiva de acessar os dados para um local específico:
|
|
43
|
+
|
|
44
|
+
```Python
|
|
45
|
+
|
|
46
|
+
endereco = "Avenida Santo Amaro, 1826"
|
|
47
|
+
codigo = geo.get_quadrant_by_address(endereco)
|
|
48
|
+
print(f"Quadrante identificado: {codigo}")
|
|
49
|
+
client.download_quadrante(codigo).save("data/raw/laz")
|
|
50
|
+
```
|
|
51
|
+
#### 4. Download por Coordenadas (Lat/Lon)
|
|
52
|
+
Para integração com GPS ou outros sistemas de mapeamento:
|
|
53
|
+
|
|
54
|
+
```Python
|
|
55
|
+
|
|
56
|
+
lat, lon = -23.598, -46.676
|
|
57
|
+
codigo = geo.get_quadrant_by_coords(lat, lon)
|
|
58
|
+
client.download_quadrante(codigo).save("data/raw/laz")
|
|
59
|
+
```
|
|
60
|
+
#### 5. Processamento por Bairro
|
|
61
|
+
Obtenha todos os quadrantes que interceptam a área de um bairro:
|
|
62
|
+
|
|
63
|
+
```Python
|
|
64
|
+
|
|
65
|
+
bairro = "Itaim Bibi"
|
|
66
|
+
codigos = geo.get_quadrants_by_neighborhood(bairro)
|
|
67
|
+
|
|
68
|
+
for cod in codigos:
|
|
69
|
+
client.download_quadrante(cod).save(f"data/raw/laz/{bairro}")
|
|
70
|
+
```
|
|
71
|
+
#### 6. Manipulando a Classe LidarQuadrante
|
|
72
|
+
A classe abstrai a complexidade dos binários baixados:
|
|
73
|
+
|
|
74
|
+
```Python
|
|
75
|
+
|
|
76
|
+
quadrante = client.download_quadrante("3316-153")
|
|
77
|
+
|
|
78
|
+
# Atributos úteis
|
|
79
|
+
print(quadrante.codigo) # "3316-153"
|
|
80
|
+
print(quadrante.esta_carregado) # True
|
|
81
|
+
print(len(quadrante.conteudo_binario)) # Tamanho em bytes do arquivo .laz
|
|
82
|
+
|
|
83
|
+
# Salva o arquivo final no disco
|
|
84
|
+
quadrante.save(dest_path="output/lidar")
|
|
85
|
+
```
|
|
86
|
+
## 📂 Estrutura do Projeto
|
|
87
|
+
```Plaintext
|
|
88
|
+
|
|
89
|
+
src/splaz/
|
|
90
|
+
├── entities.py # Representação de objetos LiDAR
|
|
91
|
+
├── downloader.py # Lógica de comunicação com GeoSampa
|
|
92
|
+
├── geocoder.py # Cálculos espaciais e geolocalização
|
|
93
|
+
└── constants.py # EPSG, URLs e parâmetros de API
|
|
94
|
+
```
|
|
95
|
+
## 🧪 Testes Automatizados
|
|
96
|
+
A biblioteca possui uma suíte de testes robusta (Unitários, Integração e E2E) que valida desde a lógica de coordenadas até o download real. Para rodar:
|
|
97
|
+
|
|
98
|
+
```Bash
|
|
99
|
+
pytest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 📄 Licença
|
|
103
|
+
Distribuído sob a licença MIT. Veja o arquivo LICENSE para mais detalhes.
|
|
104
|
+
### 🎓 Contexto Acadêmico
|
|
105
|
+
Este projeto foi desenvolvido como parte de um projeto de Iniciação Científica no Insper.
|
|
106
|
+
|
|
107
|
+
Autor: João Braga
|
|
108
|
+
|
|
109
|
+
Perfil: Aluno do 7° semestre de Engenharia da Computação.
|
|
110
|
+
|
|
111
|
+
Objetivo: Apoiar a análise de dados geoespaciais para o projeto ambiental "Vagas Verdes".
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "splaz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "API para facilitar o acesso a dados LiDAR do GeoSampa"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "João Braga"}
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests",
|
|
14
|
+
"geopandas",
|
|
15
|
+
"geopy",
|
|
16
|
+
"shapely",
|
|
17
|
+
"laspy",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
splaz-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
splaz: Uma biblioteca para facilitar o acesso e geocodificação
|
|
3
|
+
de dados LiDAR (MDS/MDT) do portal GeoSampa da cidade de São Paulo.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .entities import LidarQuadrante
|
|
7
|
+
from .downloader import SpLaz
|
|
8
|
+
from .geocoder import SpLazGeo
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"LidarQuadrante",
|
|
12
|
+
"SpLaz",
|
|
13
|
+
"SpLazGeo",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__author__ = "João Braga"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
CACHE_DIR = Path.home() / ".geosp_laz_api"
|
|
5
|
+
GRID_CACHE_PATH = CACHE_DIR / "articulacao_2020"
|
|
6
|
+
SHP_FILE_NAME = "SIRGAS_SHP_quadriculamdt.shp"
|
|
7
|
+
|
|
8
|
+
GEOSAMPA_DOWNLOAD_URL = "https://download.geosampa.prefeitura.sp.gov.br/PaginasPublicas/downloadArquivo.aspx"
|
|
9
|
+
|
|
10
|
+
GRID_MDT_PARAMS_STR = "orig=DownloadCamadas&arq=21_Articulacao%20de%20Imagens%5C%5CArticula%E7%E3o%20MDT%5C%5CShapefile%5C%5CSIRGAS_SHP_quadriculamdt&arqTipo=Shapefile"
|
|
11
|
+
|
|
12
|
+
LAZ_DOWNLOAD_PARAMS = {
|
|
13
|
+
"orig": "DownloadMapaArticulacao",
|
|
14
|
+
"arq_prefix": "MDS_2020",
|
|
15
|
+
"arq_tipo": "MAPA_ARTICULACAO"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
EPSG_SAO_PAULO = 31983
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import zipfile
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from .entities import LidarQuadrante
|
|
7
|
+
from .constants import (
|
|
8
|
+
GEOSAMPA_DOWNLOAD_URL,
|
|
9
|
+
LAZ_DOWNLOAD_PARAMS,
|
|
10
|
+
GRID_MDT_PARAMS_STR,
|
|
11
|
+
GRID_CACHE_PATH,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class SpLaz:
|
|
15
|
+
"""
|
|
16
|
+
Cliente para acessar e baixar dados LIDAR do portal GeoSampa.
|
|
17
|
+
Gerencia o download e cache da grade de articulação, além dos quadrantes específicos.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.session = requests.Session()
|
|
21
|
+
self.session.headers.update({
|
|
22
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
def ensure_grid_data(self) -> str:
|
|
26
|
+
"""Garante que a grade exista, baixando-a se necessário, e encontra o arquivo .shp."""
|
|
27
|
+
download_necessario = False
|
|
28
|
+
|
|
29
|
+
shp_files = list(GRID_CACHE_PATH.rglob("*.shp"))
|
|
30
|
+
shp_path = shp_files[0] if shp_files else None
|
|
31
|
+
|
|
32
|
+
if not shp_path:
|
|
33
|
+
download_necessario = True
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
response = self.session.head(f"{GEOSAMPA_DOWNLOAD_URL}?{GRID_MDT_PARAMS_STR}")
|
|
37
|
+
if response.status_code == 200:
|
|
38
|
+
server_last_modified = response.headers.get('Last-Modified')
|
|
39
|
+
if server_last_modified:
|
|
40
|
+
local_time = datetime.fromtimestamp(os.path.getmtime(shp_path))
|
|
41
|
+
server_time = datetime.strptime(server_last_modified, '%a, %d %b %Y %H:%M:%S GMT')
|
|
42
|
+
if server_time > local_time:
|
|
43
|
+
download_necessario = True
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"Aviso: Não foi possível checar atualizações ({e}). Usando cache local.")
|
|
46
|
+
|
|
47
|
+
if download_necessario:
|
|
48
|
+
self._download_and_extract_grid()
|
|
49
|
+
|
|
50
|
+
shp_files = list(GRID_CACHE_PATH.rglob("*.shp"))
|
|
51
|
+
if not shp_files:
|
|
52
|
+
raise FileNotFoundError("Nenhum arquivo .shp foi encontrado dentro do ZIP baixado.")
|
|
53
|
+
shp_path = shp_files[0]
|
|
54
|
+
|
|
55
|
+
return str(shp_path)
|
|
56
|
+
|
|
57
|
+
def _download_and_extract_grid(self):
|
|
58
|
+
print("Baixando grade de articulação...")
|
|
59
|
+
response = self.session.get(GEOSAMPA_DOWNLOAD_URL, params=GRID_MDT_PARAMS_STR)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
|
|
62
|
+
if not response.content.startswith(b'PK'):
|
|
63
|
+
conteudo_texto = response.text[:200]
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"O GeoSampa não retornou um arquivo ZIP. Verifique os parâmetros.\n"
|
|
66
|
+
f"Resposta do servidor: {conteudo_texto}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
os.makedirs(GRID_CACHE_PATH, exist_ok=True)
|
|
70
|
+
with zipfile.ZipFile(io.BytesIO(response.content)) as z:
|
|
71
|
+
z.extractall(GRID_CACHE_PATH)
|
|
72
|
+
print("Grade baixada e extraída com sucesso.")
|
|
73
|
+
|
|
74
|
+
def download_quadrante(self, codigo_quadra: str) -> LidarQuadrante:
|
|
75
|
+
"""
|
|
76
|
+
Baixa um quadrante LIDAR específico do GeoSampa.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
codigo_quadra (str): Código do quadrante a ser baixado.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
LidarQuadrante: Objeto contendo os dados do quadrante baixado.
|
|
83
|
+
"""
|
|
84
|
+
params = {
|
|
85
|
+
"orig": LAZ_DOWNLOAD_PARAMS["orig"],
|
|
86
|
+
"arq": f"{LAZ_DOWNLOAD_PARAMS['arq_prefix']}\\{codigo_quadra}.zip",
|
|
87
|
+
"arqTipo": LAZ_DOWNLOAD_PARAMS["arq_tipo"]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
response = self.session.get(GEOSAMPA_DOWNLOAD_URL, params=params)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
|
|
93
|
+
return self._processar_zip(codigo_quadra, response.content)
|
|
94
|
+
|
|
95
|
+
def _processar_zip(self, codigo: str, conteudo_zip: bytes) -> LidarQuadrante:
|
|
96
|
+
try:
|
|
97
|
+
with zipfile.ZipFile(io.BytesIO(conteudo_zip)) as z:
|
|
98
|
+
nome_laz = z.namelist()[0]
|
|
99
|
+
dados_laz = z.read(nome_laz)
|
|
100
|
+
return LidarQuadrante(codigo, dados_laz, nome_laz)
|
|
101
|
+
except zipfile.BadZipFile:
|
|
102
|
+
raise ValueError(f"Conteudo baixado para o quadrante {codigo} invalido.")
|
|
103
|
+
|
|
104
|
+
def download_lista(self, codigos: list) -> list:
|
|
105
|
+
return [self.download_quadrante(c) for c in codigos]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
class LidarQuadrante:
|
|
5
|
+
"""
|
|
6
|
+
Representa um quadrante LiDAR (MDS 2020) de São Paulo.
|
|
7
|
+
Encapsula o conteúdo binário e metadados do arquivo .laz.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, codigo: str, conteudo_binario: bytes = None, nome_arquivo: str = None):
|
|
10
|
+
self.codigo = codigo
|
|
11
|
+
self.conteudo_binario = conteudo_binario
|
|
12
|
+
self.nome_arquivo = nome_arquivo
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def esta_carregado(self) -> bool:
|
|
16
|
+
"""Verifica se o conteúdo binário já está na memória."""
|
|
17
|
+
return self.conteudo_binario is not None
|
|
18
|
+
|
|
19
|
+
def save(self, pasta_destino: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Salva o conteúdo binário em disco.
|
|
22
|
+
Retorna o caminho completo do arquivo salvo.
|
|
23
|
+
"""
|
|
24
|
+
if not self.esta_carregado:
|
|
25
|
+
raise ValueError(f"O quadrante {self.codigo} não possui dados em memória.")
|
|
26
|
+
|
|
27
|
+
os.makedirs(pasta_destino, exist_ok=True)
|
|
28
|
+
caminho_final = os.path.join(pasta_destino, self.nome_arquivo)
|
|
29
|
+
|
|
30
|
+
with open(caminho_final, 'wb') as f:
|
|
31
|
+
f.write(self.conteudo_binario)
|
|
32
|
+
|
|
33
|
+
return caminho_final
|
|
34
|
+
|
|
35
|
+
def to_laspy(self):
|
|
36
|
+
"""
|
|
37
|
+
Retorna um objeto laspy.read() diretamente do conteúdo em memória.
|
|
38
|
+
Requer a biblioteca 'laspy' instalada.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
import laspy
|
|
42
|
+
if self.esta_carregado:
|
|
43
|
+
return laspy.read(io.BytesIO(self.conteudo_binario))
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise ImportError("A biblioteca 'laspy' é necessária para converter dados em memória.")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def __repr__(self):
|
|
49
|
+
status = "Carregado" if self.esta_carregado else "Vazio"
|
|
50
|
+
return f"<LidarQuadrante {self.codigo} | {status}>"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
from geopy.geocoders import Nominatim
|
|
3
|
+
from shapely.geometry import Point, box
|
|
4
|
+
from .constants import EPSG_SAO_PAULO
|
|
5
|
+
from .downloader import SpLaz
|
|
6
|
+
|
|
7
|
+
class SpLazGeo:
|
|
8
|
+
"""
|
|
9
|
+
Gerenciador de geocodificação para dados LIDAR do GeoSampa.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, client: SpLaz = None):
|
|
12
|
+
self.client = client or SpLaz()
|
|
13
|
+
self._grid = None
|
|
14
|
+
self.geolocator = Nominatim(user_agent="geosp_laz_api_vagas_verdes")
|
|
15
|
+
@property
|
|
16
|
+
def grid(self) -> gpd.GeoDataFrame:
|
|
17
|
+
"""
|
|
18
|
+
Carrega e prepara o Shapefile de articulação automaticamente.
|
|
19
|
+
Mantém os dados no Ryzen 7 para buscas rápidas.
|
|
20
|
+
"""
|
|
21
|
+
if self._grid is None:
|
|
22
|
+
|
|
23
|
+
shp_path = self.client.ensure_grid_data()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
gdf = gpd.read_file(shp_path)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if gdf.crs is None:
|
|
30
|
+
|
|
31
|
+
gdf = gdf.set_crs(epsg=EPSG_SAO_PAULO)
|
|
32
|
+
elif gdf.crs.to_epsg() != EPSG_SAO_PAULO:
|
|
33
|
+
|
|
34
|
+
gdf = gdf.to_crs(epsg=EPSG_SAO_PAULO)
|
|
35
|
+
|
|
36
|
+
self._grid = gdf
|
|
37
|
+
return self._grid
|
|
38
|
+
|
|
39
|
+
def get_quadrant_by_coords(self, lat: float, lon: float) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Determina o quadrante LIDAR correspondente a coordenadas geográficas.
|
|
42
|
+
Args:
|
|
43
|
+
lat (float): Latitude em graus decimais.
|
|
44
|
+
lon (float): Longitude em graus decimais.
|
|
45
|
+
Returns:
|
|
46
|
+
str: Código do quadrante LIDAR correspondente.
|
|
47
|
+
"""
|
|
48
|
+
ponto_gps = gpd.GeoSeries([Point(lon, lat)], crs="EPSG:4326")
|
|
49
|
+
ponto_sp = ponto_gps.to_crs(epsg=EPSG_SAO_PAULO).iloc[0]
|
|
50
|
+
resultado = self.grid[self.grid.contains(ponto_sp)]
|
|
51
|
+
if resultado.empty:
|
|
52
|
+
raise ValueError(f"Coordenadas ({lat}, {lon}) fora da cobertura de SP.")
|
|
53
|
+
return str(resultado.iloc[0]['qmdt_cod'])
|
|
54
|
+
|
|
55
|
+
def get_quadrant_by_address(self, address: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Determina o quadrante LIDAR correspondente a um endereço.
|
|
58
|
+
Args:
|
|
59
|
+
address (str): Endereço completo ou parcial em São Paulo.
|
|
60
|
+
Returns:
|
|
61
|
+
str: Código do quadrante LIDAR correspondente.
|
|
62
|
+
"""
|
|
63
|
+
search_query = f"{address}, São Paulo, SP, Brazil"
|
|
64
|
+
location = self.geolocator.geocode(search_query)
|
|
65
|
+
if not location:
|
|
66
|
+
raise ValueError(f"Endereço não localizado: {address}")
|
|
67
|
+
return self.get_quadrant_by_coords(location.latitude, location.longitude)
|
|
68
|
+
|
|
69
|
+
def get_quadrants_by_neighborhood(self, neighborhood: str) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
Retorna uma lista de quadrantes LIDAR que intersectam um bairro específico.
|
|
72
|
+
Args:
|
|
73
|
+
neighborhood (str): Nome do bairro em São Paulo.
|
|
74
|
+
Returns:
|
|
75
|
+
list[str]: Lista de códigos de quadrantes LIDAR que intersectam o bairro.
|
|
76
|
+
"""
|
|
77
|
+
query = f"{neighborhood}, São Paulo, SP, Brazil"
|
|
78
|
+
location = self.geolocator.geocode(query)
|
|
79
|
+
if not location:
|
|
80
|
+
raise ValueError(f"Bairro não encontrado: {neighborhood}")
|
|
81
|
+
|
|
82
|
+
bbox = location.raw['boundingbox']
|
|
83
|
+
lat_min, lat_max, lon_min, lon_max = float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])
|
|
84
|
+
|
|
85
|
+
area_gps = gpd.GeoSeries([box(lon_min, lat_min, lon_max, lat_max)], crs="EPSG:4326")
|
|
86
|
+
area_sp = area_gps.to_crs(epsg=EPSG_SAO_PAULO).iloc[0]
|
|
87
|
+
|
|
88
|
+
intersecao = self.grid[self.grid.intersects(area_sp)]
|
|
89
|
+
|
|
90
|
+
return intersecao['qmdt_cod'].unique().tolist()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splaz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API para facilitar o acesso a dados LiDAR do GeoSampa
|
|
5
|
+
Author: João Braga
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: requests
|
|
8
|
+
Requires-Dist: geopandas
|
|
9
|
+
Requires-Dist: geopy
|
|
10
|
+
Requires-Dist: shapely
|
|
11
|
+
Requires-Dist: laspy
|
|
12
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/splaz/__init__.py
|
|
5
|
+
src/splaz/constants.py
|
|
6
|
+
src/splaz/downloader.py
|
|
7
|
+
src/splaz/entities.py
|
|
8
|
+
src/splaz/geocoder.py
|
|
9
|
+
src/splaz.egg-info/PKG-INFO
|
|
10
|
+
src/splaz.egg-info/SOURCES.txt
|
|
11
|
+
src/splaz.egg-info/dependency_links.txt
|
|
12
|
+
src/splaz.egg-info/requires.txt
|
|
13
|
+
src/splaz.egg-info/top_level.txt
|
|
14
|
+
tests/test_downloader.py
|
|
15
|
+
tests/test_end2end.py
|
|
16
|
+
tests/test_entities.py
|
|
17
|
+
tests/test_geocoder.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
splaz
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import requests_mock
|
|
3
|
+
import io
|
|
4
|
+
import zipfile
|
|
5
|
+
import os
|
|
6
|
+
from splaz.downloader import SpLaz
|
|
7
|
+
from splaz.entities import LidarQuadrante
|
|
8
|
+
from splaz.constants import GEOSAMPA_DOWNLOAD_URL
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client():
|
|
13
|
+
return SpLaz()
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def fake_zip_content():
|
|
17
|
+
buf = io.BytesIO()
|
|
18
|
+
with zipfile.ZipFile(buf,"w") as z:
|
|
19
|
+
z.writestr("3316-153.laz", b"fake_laz_binary_content")
|
|
20
|
+
return buf.getvalue()
|
|
21
|
+
|
|
22
|
+
def test_download_quadrante_sucesso(client, requests_mock, fake_zip_content):
|
|
23
|
+
requests_mock.get(GEOSAMPA_DOWNLOAD_URL, content=fake_zip_content, status_code = 200)
|
|
24
|
+
|
|
25
|
+
resultado = client.download_quadrante("3316-153")
|
|
26
|
+
|
|
27
|
+
assert isinstance(resultado,LidarQuadrante)
|
|
28
|
+
assert resultado.codigo == "3316-153"
|
|
29
|
+
assert resultado.esta_carregado is True
|
|
30
|
+
assert resultado.conteudo_binario == b"fake_laz_binary_content"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_download_quadrante_erro_404(client,requests_mock):
|
|
34
|
+
requests_mock.get(GEOSAMPA_DOWNLOAD_URL, status_code = 404 )
|
|
35
|
+
import requests
|
|
36
|
+
|
|
37
|
+
with pytest.raises(requests.exceptions.HTTPError):
|
|
38
|
+
client.download_quadrante("codigo-inexistente")
|
|
39
|
+
|
|
40
|
+
def test_processar_zip_invalido(client):
|
|
41
|
+
codigo = "test"
|
|
42
|
+
with pytest.raises(ValueError, match= f"Conteudo baixado para o quadrante {codigo} invalido."):
|
|
43
|
+
client._processar_zip(codigo, b"not_a_zip_file")
|
|
44
|
+
|
|
45
|
+
def test_download_lista_chama_multiplos(client, requests_mock, fake_zip_content):
|
|
46
|
+
requests_mock.get(GEOSAMPA_DOWNLOAD_URL, content=fake_zip_content)
|
|
47
|
+
|
|
48
|
+
codigos = ["1111-111", "2222-222"]
|
|
49
|
+
resultados = client.download_lista(codigos)
|
|
50
|
+
|
|
51
|
+
assert len(resultados) == 2
|
|
52
|
+
assert requests_mock.call_count == 2
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from splaz import SpLazGeo, SpLaz
|
|
3
|
+
|
|
4
|
+
def test_fluxo_completo_insper_vagas_verdes():
|
|
5
|
+
"""
|
|
6
|
+
TESTE DE SISTEMA (E2E):
|
|
7
|
+
Valida se a biblioteca consegue converter um endereço real,
|
|
8
|
+
identificar o quadrante e baixar o arquivo binário sem erros.
|
|
9
|
+
"""
|
|
10
|
+
# 1. Inicialização (Sem mocks!)
|
|
11
|
+
client = SpLaz()
|
|
12
|
+
geo = SpLazGeo(client=client)
|
|
13
|
+
|
|
14
|
+
# Endereço real do Insper
|
|
15
|
+
endereco = "Avenida Santo Amaro, 1826"
|
|
16
|
+
|
|
17
|
+
# 2. Execução do fluxo
|
|
18
|
+
# O Geocoder deve baixar a grade automaticamente no primeiro uso
|
|
19
|
+
codigo_quadrante = geo.get_quadrant_by_address(endereco)
|
|
20
|
+
|
|
21
|
+
# 3. Verificação do Geocoding
|
|
22
|
+
# Sabemos que o Insper fica no quadrante 3316-153
|
|
23
|
+
assert codigo_quadrante == "3316-153"
|
|
24
|
+
|
|
25
|
+
# 4. Download Real
|
|
26
|
+
# Baixamos apenas um para não sobrecarregar sua rede
|
|
27
|
+
quadrante_obj = client.download_quadrante(codigo_quadrante)
|
|
28
|
+
|
|
29
|
+
# 5. Verificações Finais
|
|
30
|
+
assert quadrante_obj.esta_carregado is True
|
|
31
|
+
assert len(quadrante_obj.conteudo_binario) > 0
|
|
32
|
+
assert quadrante_obj.nome_arquivo.endswith(".laz")
|
|
33
|
+
|
|
34
|
+
print(f"\n[E2E] Sucesso: Quadrante {codigo_quadrante} baixado com {len(quadrante_obj.conteudo_binario)} bytes.")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
from splaz.entities import LidarQuadrante
|
|
4
|
+
|
|
5
|
+
def test_lidar_quadrante_inicializacao_vazia():
|
|
6
|
+
quad = LidarQuadrante("3316-153")
|
|
7
|
+
assert quad.codigo == "3316-153"
|
|
8
|
+
assert quad.esta_carregado is False
|
|
9
|
+
assert "Vazio" in repr(quad)
|
|
10
|
+
|
|
11
|
+
def test_lidar_quadrante_inicializa_com_dados():
|
|
12
|
+
dados_falsos = b"LAZ_BINARY_DATA"
|
|
13
|
+
quad = LidarQuadrante(codigo="3316-153", conteudo_binario=dados_falsos, nome_arquivo="3316-153.laz")
|
|
14
|
+
|
|
15
|
+
assert quad.esta_carregado is True
|
|
16
|
+
assert quad.conteudo_binario == dados_falsos
|
|
17
|
+
assert "Carregado" in repr(quad)
|
|
18
|
+
|
|
19
|
+
def test_save_com_sucesso(tmp_path):
|
|
20
|
+
d = tmp_path / "data_test"
|
|
21
|
+
d.mkdir()
|
|
22
|
+
nome_arq = "test_vagas_verdes.laz"
|
|
23
|
+
dados = b"01010101"
|
|
24
|
+
|
|
25
|
+
quad = LidarQuadrante("3316-153", dados, nome_arq)
|
|
26
|
+
caminho_salvo = quad.save(str(d))
|
|
27
|
+
|
|
28
|
+
assert os.path.exists(caminho_salvo)
|
|
29
|
+
with open(caminho_salvo, "rb") as f:
|
|
30
|
+
assert f.read() == dados
|
|
31
|
+
|
|
32
|
+
def test_save_sem_dados_levanta_erro():
|
|
33
|
+
codigo = "3316-153"
|
|
34
|
+
quad = LidarQuadrante(codigo)
|
|
35
|
+
with pytest.raises(ValueError, match=f"O quadrante {codigo} não possui dados em memória."):
|
|
36
|
+
quad.save("pasta_qualquer")
|
|
37
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import geopandas as gpd
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from shapely.geometry import Point, Polygon, box
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
from splaz.geocoder import SpLazGeo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_client():
|
|
11
|
+
client = MagicMock()
|
|
12
|
+
client.ensure_grid_data.return_value = "caminho/falso/grade.shp"
|
|
13
|
+
return client
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_grid_gdf():
|
|
17
|
+
"""Cria um GeoDataFrame minúsculo para simular a grade de SP."""
|
|
18
|
+
# Quadrado simples representando um quadrante fictício
|
|
19
|
+
poly = Polygon([(0, 0), (1000, 0), (1000, 1000), (0, 1000)])
|
|
20
|
+
gdf = gpd.GeoDataFrame(
|
|
21
|
+
{'qmdt_cod': ['3316-153'], 'geometry': [poly]},
|
|
22
|
+
crs="EPSG:31983"
|
|
23
|
+
)
|
|
24
|
+
return gdf
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_quadrant_by_coords_sucesso(mock_client, mock_grid_gdf):
|
|
28
|
+
with patch("geopandas.read_file", return_value = mock_grid_gdf):
|
|
29
|
+
geo = SpLazGeo(client=mock_client)
|
|
30
|
+
|
|
31
|
+
with patch.object(gpd.GeoSeries, "to_crs") as mock_to_crs:
|
|
32
|
+
mock_to_crs.return_value = gpd.GeoSeries([Point(500,500)])
|
|
33
|
+
|
|
34
|
+
codigo = geo.get_quadrant_by_coords(-23.5,-46.6)
|
|
35
|
+
assert codigo == '3316-153'
|
|
36
|
+
|
|
37
|
+
def test_get_quadrant_by_address_sucesso(mock_client, mock_grid_gdf):
|
|
38
|
+
with patch("geopandas.read_file", return_value=mock_grid_gdf):
|
|
39
|
+
geo = SpLazGeo(client=mock_client)
|
|
40
|
+
|
|
41
|
+
# Mock do retorno do Nominatim (Geopy)
|
|
42
|
+
mock_location = MagicMock()
|
|
43
|
+
mock_location.latitude = -23.59
|
|
44
|
+
mock_location.longitude = -46.67
|
|
45
|
+
|
|
46
|
+
with patch.object(geo.geolocator, "geocode", return_value=mock_location):
|
|
47
|
+
with patch.object(geo, "get_quadrant_by_coords", return_value="3316-153"):
|
|
48
|
+
codigo = geo.get_quadrant_by_address("Rua Quatá, 300")
|
|
49
|
+
assert codigo == "3316-153"
|
|
50
|
+
|
|
51
|
+
def test_get_quadrants_by_neighborhood_bbox(mock_client,mock_grid_gdf):
|
|
52
|
+
with patch("geopandas.read_file", return_value=mock_grid_gdf):
|
|
53
|
+
geo = SpLazGeo(client=mock_client)
|
|
54
|
+
|
|
55
|
+
# 1. Simula o retorno do Nominatim (Bairro real)
|
|
56
|
+
mock_location = MagicMock()
|
|
57
|
+
mock_location.raw = {
|
|
58
|
+
'boundingbox': ['-23.6', '-23.5', '-46.7', '-46.6']
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
with patch.object(geo.geolocator, "geocode", return_value=mock_location):
|
|
62
|
+
# 2. Mockamos a conversão de coordenadas para que a área resultante
|
|
63
|
+
# coincida com o nosso quadrante de teste (0 a 1000)
|
|
64
|
+
with patch.object(gpd.GeoSeries, "to_crs") as mock_to_crs:
|
|
65
|
+
# Criamos um polígono que sobrepõe o nosso mock_grid_gdf (0,0 a 1000,1000)
|
|
66
|
+
overlap_box = box(100, 100, 500, 500)
|
|
67
|
+
mock_to_crs.return_value = gpd.GeoSeries([overlap_box])
|
|
68
|
+
|
|
69
|
+
quadrantes = geo.get_quadrants_by_neighborhood("Vila Olímpia")
|
|
70
|
+
|
|
71
|
+
# Agora deve funcionar, pois as geometrias se interceptam!
|
|
72
|
+
assert isinstance(quadrantes, list)
|
|
73
|
+
assert "3316-153" in quadrantes
|