Add Leaflet interactive map, tile generator compose, auto-sync cache, deploy docs
index.py rewritten as a continuous Leaflet map: rotated L93 tiles, stackable visualization layers (per-layer opacity, drag-reorder persisted in localStorage), tile info panel, live rebuild after each tile during a run. Leaflet is vendored in assets/vendor/ so the map works fully offline; georeferencing falls back rasterio -> pyproj -> affine so the lightweight webapp (no GDAL) is supported. docker-compose.worker.yml adds the tile generator service (full image + GPU) that remote webapps call via LIDAR_GENERATION_URL, plus a one-shot process profile. webapp.py gains LIDAR_AUTO_SYNC_SECONDS periodic cache refresh and LIDAR_REGEN_CIDR restricting generation to the local network. run.sh --serve-webapp now mounts ~/.ssh read-only so the rsync sync works. docs/DEPLOY_WEBAPP.md completed for Raspberry Pi deployment: prerequisites, git clone install, SSH key setup, first sync, update procedure and troubleshooting.
This commit is contained in:
@ -3,6 +3,7 @@
|
||||
- install: `docker build -t lidar-lidar .` (deps baked into image)
|
||||
- build: `docker build -t lidar-lidar .`
|
||||
- build webapp légère (Raspberry Pi, déploiement 2 machines — cf. `docs/DEPLOY_WEBAPP.md`): `docker compose -f docker-compose.webapp.yml up -d --build` (image `Dockerfile.webapp`, sans PDAL/GPU)
|
||||
- build générateur de tuiles (machine de traitement): `docker compose -f docker-compose.worker.yml up -d --build` (service `worker`, API pour les webapp distantes)
|
||||
- test all: `./run.sh --test` (rebuild automatique de l'image avant les tests ; en `docker run` direct, rebuild manuellement d'abord)
|
||||
- test file: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>`
|
||||
- test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>`
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# La carte et les vignettes sont servies localement ; la génération de tuiles
|
||||
# (téléchargement IGN + traitement PDAL/GPU) est déléguée à la machine de
|
||||
# traitement = service `serve` du docker-compose.yml de cette machine.
|
||||
# traitement = service `worker` du docker-compose.worker.yml de cette machine.
|
||||
#
|
||||
# docker compose -f docker-compose.webapp.yml up -d --build
|
||||
# docker compose -f docker-compose.webapp.yml logs -f webapp
|
||||
@ -27,11 +27,22 @@ services:
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
- LIDAR_OUTPUT_DIR=/data/output
|
||||
# Machine de traitement (service `serve` de son docker-compose.yml)
|
||||
- LIDAR_GENERATION_URL=http://192.168.1.50:8973
|
||||
# Adresse du backend de génération des tuiles (machine de traitement,
|
||||
# service `worker` de son docker-compose.worker.yml) — à définir :
|
||||
# LIDAR_GENERATION_URL=http://<ip-machine-puissante>:8973
|
||||
# Sans cette variable : consultation autonome du cache local, aucune
|
||||
# génération possible depuis l'interface.
|
||||
# - LIDAR_GENERATION_URL=http://192.168.1.50:8973
|
||||
# Jeton partagé si LIDAR_API_TOKEN est défini sur la machine de traitement
|
||||
# - LIDAR_REMOTE_TOKEN=change-moi
|
||||
# Ramène les tuiles traitées + sidecars DTM (les vignettes sont
|
||||
# régénérées localement par /api/sync, ne pas les synchroniser)
|
||||
# - LIDAR_SYNC_CMD=rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/
|
||||
# Cache local auto-entretenu : resynchronisation + vignettes toutes les
|
||||
# N secondes (0/défaut = seulement à la demande, bouton ↻ ou /api/sync)
|
||||
# - LIDAR_AUTO_SYNC_SECONDS=600
|
||||
# Réseau autorisé à lancer les générations depuis la carte (défaut
|
||||
# 192.168.1.0/24 ; chaîne vide = restriction levée). Les autres clients
|
||||
# consultent la carte en lecture seule (boutons masqués, API 403).
|
||||
# - LIDAR_REGEN_CIDR=192.168.1.0/24
|
||||
restart: unless-stopped
|
||||
|
||||
57
docker-compose.worker.yml
Normal file
57
docker-compose.worker.yml
Normal file
@ -0,0 +1,57 @@
|
||||
# Machine de traitement — générateur de tuiles (cf. docs/DEPLOY_WEBAPP.md).
|
||||
#
|
||||
# Tourne sur la machine puissante (GPU + PDAL) et expose l'API que les
|
||||
# webapp distantes (Raspberry Pi, docker-compose.webapp.yml) appellent :
|
||||
# dessin d'une zone → téléchargement IGN + traitement GPU ici, les tuiles
|
||||
# sont relues par la webapp via son LIDAR_SYNC_CMD (rsync).
|
||||
#
|
||||
# docker compose -f docker-compose.worker.yml up -d --build
|
||||
# docker compose -f docker-compose.worker.yml logs -f worker
|
||||
# docker compose -f docker-compose.worker.yml down
|
||||
#
|
||||
# Traitement batch ponctuel des dalles présentes dans input/ :
|
||||
# docker compose -f docker-compose.worker.yml run --rm --build process [-r 0.5,0.2 | --force]
|
||||
services:
|
||||
# Générateur de tuiles : API de génération (webapp complète, GPU)
|
||||
worker:
|
||||
build: .
|
||||
image: lidar-lidar
|
||||
container_name: lidar-worker
|
||||
init: true
|
||||
user: "1000:1000"
|
||||
gpus: all # retirer cette ligne sur une machine sans GPU
|
||||
ports:
|
||||
- "8973:8973" # joignable par la webapp distante (LIDAR_GENERATION_URL)
|
||||
volumes:
|
||||
# input/ en écriture : l'API y télécharge les dalles IGN manquantes
|
||||
- ./input:/data/input
|
||||
- ./output:/data/output
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
- LIDAR_INPUT_DIR=/data/input
|
||||
- LIDAR_OUTPUT_DIR=/data/output
|
||||
# Les générations lancées depuis une webapp distante utilisent le GPU
|
||||
- LIDAR_GPU=1
|
||||
- LIDAR_WORKERS=2
|
||||
# Protéger l'API si le réseau n'est pas de confiance : même valeur que
|
||||
# LIDAR_REMOTE_TOKEN sur chaque webapp distante (sinon, laisser commenté)
|
||||
# - LIDAR_API_TOKEN=change-moi
|
||||
command: python3 -m uvicorn lidar_pipeline.webapp:app --host 0.0.0.0 --port 8973
|
||||
restart: unless-stopped
|
||||
|
||||
# Traitement ponctuel des dalles input/ (une passe puis arrêt)
|
||||
process:
|
||||
build: .
|
||||
image: lidar-lidar
|
||||
container_name: lidar-process
|
||||
init: true
|
||||
user: "1000:1000"
|
||||
gpus: all # retirer cette ligne sur une machine sans GPU
|
||||
volumes:
|
||||
- ./input:/data/input
|
||||
- ./output:/data/output
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
command: ["python3", "-m", "lidar_pipeline", "/data/input", "-o", "/data/output", "-r", "0.5,0.2", "-g", "all"]
|
||||
profiles:
|
||||
- process
|
||||
@ -12,21 +12,32 @@ Navigateur ──HTTP──▶ Raspberry Pi (image légère, Dockerfile.webapp)
|
||||
│ └─ transmis à ──▶ machine de traitement
|
||||
│ /api/sync : rsync output/ ──◀── machine puissante
|
||||
▼
|
||||
Machine puissante (image complète, docker-compose.yml
|
||||
service `serve`) : téléchargement IGN + pipeline GPU
|
||||
Machine de traitement (image complète,
|
||||
docker-compose.worker.yml service `worker`) :
|
||||
téléchargement IGN + pipeline GPU (générateur de tuiles)
|
||||
```
|
||||
|
||||
## Machine de traitement (puissante)
|
||||
## Machine de traitement (générateur de tuiles)
|
||||
|
||||
Le service `serve` existant joue le rôle de worker : il accepte les demandes
|
||||
de génération envoyées par la webapp du Pi.
|
||||
Le service `worker` joue le rôle de générateur : il accepte les demandes
|
||||
de génération envoyées par les webapp distantes (téléchargement IGN +
|
||||
traitement PDAL/GPU) et expose les tuiles produites pour le rsync.
|
||||
|
||||
```bash
|
||||
docker compose up -d --build serve # carte + API sur http://<ip-machine>:8973
|
||||
docker compose -f docker-compose.worker.yml up -d --build # API sur http://<ip-machine>:8973
|
||||
docker compose -f docker-compose.worker.yml logs -f worker
|
||||
```
|
||||
|
||||
Optionnel mais recommandé si le réseau n'est pas de confiance : protéger les
|
||||
routes mutantes avec un jeton partagé — décommenter dans `docker-compose.yml` :
|
||||
Traitement batch ponctuel des dalles déjà présentes dans `input/` :
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.worker.yml run --rm --build process [-r 0.5,0.2 | --force]
|
||||
```
|
||||
|
||||
Sur une machine sans GPU : retirer les lignes `gpus: all` (traitement CPU,
|
||||
plus lent). Optionnel mais recommandé si le réseau n'est pas de confiance :
|
||||
protéger l'API avec un jeton partagé — décommenter dans
|
||||
`docker-compose.worker.yml` :
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
@ -35,26 +46,85 @@ environment:
|
||||
|
||||
## Raspberry Pi (webapp légère)
|
||||
|
||||
### 1. Copier le code sur le Pi
|
||||
### 0. Prérequis sur le Pi
|
||||
|
||||
- **Architecture** : image construite nativement sur la machine (ARM64 ou
|
||||
x86_64, vérifier avec `uname -m`). Le build se fait sur le Pi lui-même
|
||||
(`Dockerfile.webapp` : base `python:3.12-slim`, ~200 Mo, sans PDAL/GDAL).
|
||||
- **Docker + plugin compose** : installation officielle
|
||||
[docs.docker.com/engine/install](https://docs.docker.com/engine/install/)
|
||||
(tester avec `docker compose version`).
|
||||
- **rsync/ssh côté client** : inutile sur l'hôte — ils sont dans l'image —
|
||||
mais la **clé SSH** de l'hôte est montée dans le conteneur.
|
||||
- **Espace disque** : prévoir la taille du cache de tuiles (compter la
|
||||
taille de `output/` sur la machine de traitement, ~quelques dizaines de
|
||||
Mo par dalle et par résolution).
|
||||
|
||||
### 1. Copier le code sur le Pi (git clone)
|
||||
|
||||
```bash
|
||||
git clone <ce-dépôt> lidar && cd lidar # ou rsync du poste de dev
|
||||
git clone ssh://git@git.example.fr:2222/code_public/lidar_rendu.git lidar
|
||||
cd lidar
|
||||
```
|
||||
|
||||
Le clone SSH utilise la même clé que le rsync (`~/.ssh`, voir ci-dessous) ;
|
||||
si le serveur git n'est pas encore connu, faire une première connexion pour
|
||||
accepter son empreinte. `output/` et `input/` sont ignorés par git : le
|
||||
dépôt ne contient que le code, le cache de tuiles se remplit ensuite par
|
||||
rsync (premier chargement, section 4).
|
||||
|
||||
### 2. Accès SSH pour le rsync (la Pi tire les tuiles traitées)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 # si pas encore de clé
|
||||
ssh-copy-id lidar@<ip-machine> # compte lecture sur output/
|
||||
ssh lidar@<ip-machine> exit # 1re connexion : enregistre known_hosts
|
||||
```
|
||||
|
||||
La dernière commande évite le prompt « authenticity of host » pendant le
|
||||
rsync (le conteneur ne peut pas répondre interactivement).
|
||||
|
||||
Sur la machine puissante, le dossier `output/` doit être lisible par ce
|
||||
compte (ex. `/srv/lidar/output` si vous préférez un chemin dédié — adaptez
|
||||
LIDAR_SYNC_CMD).
|
||||
|
||||
Avec `run.sh`, `~/.ssh` (clé + known_hosts) est monté automatiquement en
|
||||
lecture seule dans le conteneur ; avec `docker compose`, le montage
|
||||
équivalent est à décommenter dans `docker-compose.webapp.yml` (voir
|
||||
l'option b ci-dessous).
|
||||
|
||||
### 3. Configurer et lancer
|
||||
|
||||
Éditer `docker-compose.webapp.yml` :
|
||||
Deux façons, au choix :
|
||||
|
||||
**a. `run.sh` (le plus simple)** — conteneur webapp seul, cache local des
|
||||
tuiles ; les variables se passent depuis l'environnement hôte, `~/.ssh`
|
||||
est monté automatiquement :
|
||||
|
||||
```bash
|
||||
LIDAR_GENERATION_URL=http://192.168.1.50:8973 \
|
||||
LIDAR_REMOTE_TOKEN=un-secret-à-partager \
|
||||
LIDAR_SYNC_CMD="rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/" \
|
||||
LIDAR_AUTO_SYNC_SECONDS=600 \
|
||||
./run.sh --serve-webapp # port 8973, ou --serve-webapp 9000
|
||||
```
|
||||
|
||||
`LIDAR_AUTO_SYNC_SECONDS` entretient le cache tout seul : toutes les N
|
||||
secondes, rsync ramène les nouvelles tuiles et les vignettes manquantes
|
||||
sont régénérées (les mtimes évitent tout recalcul inutile). Sans cette
|
||||
variable, le cache se rafraîchit à la demande : bouton ↻, fin d'un run,
|
||||
ou `POST /api/sync`.
|
||||
|
||||
`LIDAR_REGEN_CIDR` restreint le lancement des générations (`POST
|
||||
/api/generate` : zones, complétion, régénération) aux clients dont l'IP
|
||||
est dans le réseau indiqué — défaut `192.168.1.0/24`, chaîne vide pour
|
||||
lever la restriction. L'IP est celle de la connexion (conservée par le
|
||||
DNAT Docker pour les clients du LAN) ; derrière un reverse proxy local
|
||||
(dans le réseau autorisé), `X-Forwarded-For` désigne le client réel.
|
||||
Les autres clients consultent la carte normalement : les boutons de
|
||||
génération sont masqués et l'API répond 403.
|
||||
|
||||
**b. `docker compose`** — éditer `docker-compose.webapp.yml` :
|
||||
|
||||
- `LIDAR_GENERATION_URL` : `http://<ip-machine>:8973`
|
||||
- `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine
|
||||
@ -66,6 +136,14 @@ LIDAR_SYNC_CMD).
|
||||
- LIDAR_SYNC_CMD=rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/
|
||||
```
|
||||
|
||||
Décommenter aussi le montage de la clé SSH (rsync) :
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./output:/data/output
|
||||
- ~/.ssh:/home/lidar/.ssh:ro
|
||||
```
|
||||
|
||||
Les vignettes (`index_thumbs/`, `index_subtiles/`) ne se synchronisent pas :
|
||||
elles sont régénérées sur place par le Pi (`/api/sync` → `build_index`),
|
||||
c'est le seul travail lourd qu'il fait. Exclure `*.tif` évite de copier des
|
||||
@ -79,6 +157,10 @@ docker compose -f docker-compose.webapp.yml up -d --build
|
||||
|
||||
La carte est sur `http://<ip-pi>:8973/`.
|
||||
|
||||
`run.sh` lance le conteneur au premier plan (Ctrl-C arrête tout) : pratique
|
||||
pour tester. Pour une installation permanente, préférer `docker compose`
|
||||
(`restart: unless-stopped` : la carte revient après un reboot du Pi).
|
||||
|
||||
### 4. Premier chargement
|
||||
|
||||
Si aucune tuile n'a encore été synchronisée, déclencher une première fois :
|
||||
@ -90,6 +172,42 @@ curl -X POST http://<ip-pi>:8973/api/sync
|
||||
(puis attendre la fin : `curl http://<ip-pi>:8973/api/sync` → `"running":
|
||||
false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
|
||||
|
||||
## Mise à jour
|
||||
|
||||
Sur le Pi, après un `git pull` (ou rsync du code) :
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.webapp.yml up -d --build # rebuild + redémarrage
|
||||
```
|
||||
|
||||
Avec `run.sh --serve-webapp`, l'image est reconstruite automatiquement à
|
||||
chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
|
||||
(`output/`) n'est pas affecté par les mises à jour d'image.
|
||||
|
||||
## Dépannage
|
||||
|
||||
- **État d'une sync** : `curl http://<ip-pi>:8973/api/sync` → `running`,
|
||||
`phase` (`sync` puis `index`) et `error` (dernière erreur, ex. fin de
|
||||
journal rsync en cas d'échec). Les logs du conteneur :
|
||||
`docker logs lidar-webapp` (ou `docker compose -f docker-compose.webapp.yml
|
||||
logs -f webapp`).
|
||||
- **`Permission denied (publickey)` pendant la sync** : refaire
|
||||
`ssh-copy-id lidar@<ip-machine>` et une connexion manuelle pour
|
||||
renseigner `known_hosts` ; vérifier que `~/.ssh` est monté dans le
|
||||
conteneur (automatique avec `run.sh`, à décommenter en compose).
|
||||
- **Boutons de génération masqués / `403` sur `/api/generate`** : l'IP du
|
||||
client n'est pas dans `LIDAR_REGEN_CIDR` (défaut `192.168.1.0/24`).
|
||||
Adapter la variable ou la vider pour lever la restriction.
|
||||
- **« Machine de traitement injoignable »** : vérifier le service `worker`
|
||||
(`docker compose -f docker-compose.worker.yml logs -f worker`) et
|
||||
`LIDAR_GENERATION_URL` (IP + port 8973). La consultation de la carte
|
||||
reste possible avec le cache local.
|
||||
- **Carte vide après sync** : attendre la fin du rebuild (`GET /api/sync` →
|
||||
`"running": false`) puis recharger la page ; le bouton ↻ de la carte
|
||||
enchaîne sync + vignettes automatiquement.
|
||||
- **Port 8973 déjà pris** : `--serve-webapp 9000` avec `run.sh`, ou changer
|
||||
le mapping `ports:` en compose (le conteneur écoute toujours sur 8973).
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
- Dessiner une zone (bouton « + Zone ») sur la carte du Pi envoie la demande
|
||||
@ -98,9 +216,19 @@ false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
|
||||
machine distante en direct.
|
||||
- À la fin du run, le navigateur appelle `/api/sync` : rsync ramène les
|
||||
images, le Pi régénère vignettes/sous-tuiles/index.html, puis recharge la
|
||||
carte. Le bouton ↻ relance le même cycle à tout moment.
|
||||
carte. Le bouton ↻ relance le même cycle à tout moment. Avec
|
||||
`LIDAR_AUTO_SYNC_SECONDS`, le conteneur webapp seul maintient aussi son
|
||||
cache périodiquement (utile quand personne ne consulte la carte).
|
||||
- Sans machine de traitement configurée, `./run.sh --serve-webapp` sert la
|
||||
carte en lecture seule depuis `output/` (cache figé, aucun traitement
|
||||
possible depuis l'interface).
|
||||
- **Autonomie** : une fois les tuiles chargées dans le cache local, la
|
||||
webapp fonctionne seule — Leaflet est vendorisé dans `assets/vendor/`
|
||||
(aucun CDN), tuiles/vignettes/infos sont lues dans `output/`, et un
|
||||
backend injoignable masque simplement les boutons de génération sans
|
||||
affecter la consultation.
|
||||
- Machine de traitement seule (sans Pi) : rien ne change, `--serve` /
|
||||
`docker compose up serve` fonctionne comme avant en local.
|
||||
`docker compose up -d --build serve` fonctionne comme avant en local (tout-en-un), et `docker-compose.worker.yml` installe le générateur seul.
|
||||
- Si la machine de traitement est éteinte, la carte du Pi reste consultable
|
||||
(données synchronisées) ; seuls les nouveaux traitements sont indisponibles
|
||||
(message « machine de traitement injoignable »).
|
||||
@ -108,8 +236,11 @@ false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
|
||||
## Références
|
||||
|
||||
- `lidar_pipeline/webapp.py` : proxy distant (`LIDAR_GENERATION_URL`),
|
||||
`/api/sync`, jeton `LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`.
|
||||
`/api/sync`, cache local périodique (`LIDAR_AUTO_SYNC_SECONDS`), jeton
|
||||
`LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`, restriction des générations au
|
||||
réseau local (`LIDAR_REGEN_CIDR`).
|
||||
- `lidar_pipeline/index.py` : vignettes/index sans GDAL (pyproj ou repli
|
||||
affine), recharge après sync.
|
||||
- `Dockerfile.webapp`, `docker-compose.webapp.yml` : image légère ARM64
|
||||
- `docker-compose.worker.yml` : générateur de tuiles (machine puissante).
|
||||
- `Dockerfile.webapp`, `docker-compose.webapp.yml` : webapp légère ARM64
|
||||
(FastAPI + Pillow AVIF natif + pyproj, ~200 Mo).
|
||||
|
||||
BIN
lidar_pipeline/assets/vendor/leaflet/images/layers-2x.png
vendored
Normal file
BIN
lidar_pipeline/assets/vendor/leaflet/images/layers-2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
lidar_pipeline/assets/vendor/leaflet/images/layers.png
vendored
Normal file
BIN
lidar_pipeline/assets/vendor/leaflet/images/layers.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-icon-2x.png
vendored
Normal file
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-icon-2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-icon.png
vendored
Normal file
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-shadow.png
vendored
Normal file
BIN
lidar_pipeline/assets/vendor/leaflet/images/marker-shadow.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
661
lidar_pipeline/assets/vendor/leaflet/leaflet.css
vendored
Normal file
661
lidar_pipeline/assets/vendor/leaflet/leaflet.css
vendored
Normal file
@ -0,0 +1,661 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
6
lidar_pipeline/assets/vendor/leaflet/leaflet.js
vendored
Normal file
6
lidar_pipeline/assets/vendor/leaflet/leaflet.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -257,14 +257,29 @@ def test_build_index_generates_html(tmp_path):
|
||||
assert "t.started === s.started" in app_js
|
||||
# Bouton « Compléter » : visualisations manquantes des dalles présentes
|
||||
# dans input/ (scope all_missing envoyé au preview et au generate).
|
||||
# « + Zone » et « Compléter » ne vivent que dans l'onglet Génération,
|
||||
# jamais dans la barre d'outils flottante de la carte.
|
||||
assert 'id="btnComplete"' in content
|
||||
map_tools = content.split('id="mapTools"', 1)[1].split('</nav>', 1)[0]
|
||||
assert 'btnGenZone' not in map_tools and 'btnComplete' not in map_tools
|
||||
tab_gen = content.split('id="tab-gen"', 1)[1].split('id="tab-tile"', 1)[0]
|
||||
assert 'id="btnGenZone"' in tab_gen and 'id="btnComplete"' in tab_gen
|
||||
assert "all_missing" in app_js and app_js.count("all_missing") >= 2
|
||||
# Dessin de zone au 1er plan : pane dédié au-dessus des tuiles (marqueurs
|
||||
# Leaflet, pane 600) pour rectangle et cellules fantômes.
|
||||
assert "createPane('genDraw')" in app_js
|
||||
assert app_js.count("pane: 'genDraw'") >= 3 # rectangle + 2 jeux de cellules
|
||||
assert 'assets/app.css' in content
|
||||
# Bouton d'actualisation des couches (détection des nouveautés sur disque) :
|
||||
# le ↻ resynchronise puis reconstruit la carte (mode webapp distante).
|
||||
assert 'id="layerRefresh"' in content
|
||||
assert "/api/layers" in app_js and "/api/sync" in app_js
|
||||
assert 'assets/app.js' in content
|
||||
# Autonomie hors ligne : Leaflet vendorisé localement, aucun CDN
|
||||
assert 'assets/vendor/leaflet/leaflet.js' in content
|
||||
assert 'assets/vendor/leaflet/leaflet.css' in content
|
||||
assert 'unpkg.com' not in content
|
||||
assert (assets / 'vendor' / 'leaflet' / 'leaflet.js').is_file()
|
||||
# Fin d'un run de génération : synchronisation puis rechargement (le
|
||||
# rechargement direct reste en secours si /api/sync est absent).
|
||||
assert "syncThenReload" in app_js
|
||||
@ -284,6 +299,23 @@ def test_build_index_generates_html(tmp_path):
|
||||
assert len(thumbs) >= 2 # au moins hillshade pour chaque tuile
|
||||
|
||||
|
||||
def test_mid_thumbnails_generated(tmp_path):
|
||||
"""Vignette intermédiaire 640 px générée et référencée dans l'index."""
|
||||
from lidar_pipeline.index import build_index
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
vis_dir = output_dir / "visualisations"
|
||||
vis_dir.mkdir(parents=True)
|
||||
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
|
||||
|
||||
assert build_index(output_dir) is not None
|
||||
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||
viz = data['tiles'][0]['viz']['hillshade_multi']
|
||||
assert 'mid' in viz
|
||||
assert (output_dir / viz['mid']).exists()
|
||||
assert viz['mid'].endswith("_mid.jpg")
|
||||
|
||||
|
||||
def test_subtiled_viz_covers_generation_choices():
|
||||
"""Les viz générables depuis la carte sont sous-tuilées (affichage fluide)."""
|
||||
from lidar_pipeline.index import _CARTO_SUBTILED_VIZ
|
||||
@ -387,6 +419,8 @@ def test_build_subtiles_regenerates_stale_crops(tmp_path):
|
||||
hill_avif = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_hillshade_multi_0_0.avif"
|
||||
aspect_avif = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_aspect_0_0.avif"
|
||||
assert hill_avif.exists() and aspect_avif.exists()
|
||||
hill_mid = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_hillshade_multi_0_0_mid.webp"
|
||||
assert hill_mid.exists()
|
||||
m_hill_1 = hill_avif.stat().st_mtime
|
||||
m_aspect_1 = aspect_avif.stat().st_mtime
|
||||
|
||||
|
||||
@ -583,3 +583,81 @@ def test_require_token():
|
||||
# Sans jeton configuré : tout passe (LAN de confiance)
|
||||
webapp.API_TOKEN = None
|
||||
assert webapp._require_token(None) is None
|
||||
|
||||
|
||||
class _FakeRequest:
|
||||
"""Requête minimale pour tester la restriction par IP (peer + XFF)."""
|
||||
|
||||
def __init__(self, host=None, xff=None):
|
||||
self.client = type("C", (), {"host": host})() if host else None
|
||||
self.headers = {"x-forwarded-for": xff} if xff else {}
|
||||
|
||||
|
||||
def test_generate_restricted_to_lan_cidr():
|
||||
"""/api/generate n'est autorisé que depuis LIDAR_REGEN_CIDR (défaut 192.168.1.0/24)."""
|
||||
from fastapi import HTTPException
|
||||
import lidar_pipeline.webapp as webapp
|
||||
old = webapp.REGEN_CIDR
|
||||
webapp.REGEN_CIDR = "192.168.1.0/24"
|
||||
try:
|
||||
# IP du réseau autorisé
|
||||
webapp._require_lan_for_generation(_FakeRequest(host="192.168.1.42"))
|
||||
# Reverse proxy local (pair dans le réseau) : X-Forwarded-For désigne
|
||||
# le client réel derrière lui — refusé s'il est hors réseau
|
||||
webapp._require_lan_for_generation(
|
||||
_FakeRequest(host="192.168.1.50", xff="192.168.1.10, 192.168.1.50"))
|
||||
for host, xff in (("10.0.0.5", None), ("8.8.8.8", None),
|
||||
("192.168.3.1", None), ("2001:db8::1", None),
|
||||
# XFF forgé par un client externe : ignoré (pair hors réseau)
|
||||
("8.8.8.8", "192.168.1.5"),
|
||||
# Client réel externe derrière le proxy local
|
||||
("192.168.1.50", "8.8.8.8"),
|
||||
# IP absente
|
||||
(None, None)):
|
||||
try:
|
||||
webapp._require_lan_for_generation(_FakeRequest(host=host, xff=xff))
|
||||
assert False, f"403 attendu pour host={host!r} xff={xff!r}"
|
||||
except HTTPException as e:
|
||||
assert e.status_code == 403
|
||||
assert "réseau local" in e.detail
|
||||
finally:
|
||||
webapp.REGEN_CIDR = old
|
||||
|
||||
|
||||
def test_generate_cidr_disabled(monkeypatch):
|
||||
"""LIDAR_REGEN_CIDR vide = restriction levée (tout client autorisé)."""
|
||||
import lidar_pipeline.webapp as webapp
|
||||
monkeypatch.setattr(webapp, "REGEN_CIDR", "")
|
||||
webapp._require_lan_for_generation(_FakeRequest(host="8.8.8.8"))
|
||||
webapp._require_lan_for_generation(_FakeRequest())
|
||||
|
||||
|
||||
def test_status_exposes_regen_allowed():
|
||||
"""/api/status indique à l'interface si le client peut générer."""
|
||||
import lidar_pipeline.webapp as webapp
|
||||
assert webapp.status(_FakeRequest(host="192.168.1.7"))["regen_allowed"] is True
|
||||
assert webapp.status(_FakeRequest(host="8.8.8.8"))["regen_allowed"] is False
|
||||
|
||||
|
||||
def test_auto_sync_once_skips_when_busy(monkeypatch):
|
||||
"""Le cycle de cache ne lance rien si un rebuild tourne déjà."""
|
||||
import time as _time
|
||||
import lidar_pipeline.webapp as webapp
|
||||
import lidar_pipeline.index as index_mod
|
||||
calls = []
|
||||
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
|
||||
monkeypatch.setattr(webapp, "SYNC_CMD", "exit 0")
|
||||
# Un cycle démarre bien (sync + rebuild)
|
||||
assert webapp._auto_sync_once() is True
|
||||
for _ in range(200):
|
||||
if not webapp.rebuild_status()["running"]:
|
||||
break
|
||||
_time.sleep(0.05)
|
||||
assert calls
|
||||
# Rebuild artificiellement occupé : le cycle suivant est sauté sans erreur
|
||||
webapp._rebuild["running"] = True
|
||||
try:
|
||||
assert webapp._auto_sync_once() is False
|
||||
assert len(calls) == 1
|
||||
finally:
|
||||
webapp._rebuild["running"] = False
|
||||
|
||||
@ -41,6 +41,7 @@ carte localement. LIDAR_API_TOKEN (machine de traitement) + LIDAR_REMOTE_TOKEN
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
@ -51,7 +52,7 @@ from pathlib import Path
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -73,6 +74,17 @@ API_TOKEN = os.environ.get("LIDAR_API_TOKEN") or None
|
||||
# Commande shell qui ramène les tuiles traitées dans output/ local (rsync
|
||||
# depuis la machine de traitement). Exécutée par /api/sync avant le rebuild.
|
||||
SYNC_CMD = os.environ.get("LIDAR_SYNC_CMD") or None
|
||||
# Réseau autorisé à lancer la (re)génération des tuiles (/api/generate) :
|
||||
# CIDR en notation standard, vide = restriction levée. L'IP du client est
|
||||
# l'IP de connexion (conservée par le DNAT Docker) ; si elle est dans le
|
||||
# réseau autorisé, X-Forwarded-For est honoré (reverse proxy local qui
|
||||
# expose la carte au-dehors — un client distant ne peut pas le forger
|
||||
# puisque sa connexion directe est déjà hors réseau).
|
||||
REGEN_CIDR = (os.environ.get("LIDAR_REGEN_CIDR", "192.168.1.0/24") or "").strip()
|
||||
# Cache local périodique : relance la synchronisation + le rebuild toutes
|
||||
# les N secondes (0 = désactivé). Sert au conteneur webapp seul, qui maintient
|
||||
# sa copie des tuiles de la machine de traitement.
|
||||
AUTO_SYNC_SECONDS = float(os.environ.get("LIDAR_AUTO_SYNC_SECONDS", "0") or 0)
|
||||
|
||||
# Résolutions générées par /api/generate (cf. _build_command). La détection des
|
||||
# tuiles à compléter exige ces mêmes résolutions : une tuile n'est complète
|
||||
@ -81,6 +93,27 @@ GENERATE_RESOLUTIONS = (0.5, 0.2)
|
||||
|
||||
app = FastAPI(title="Carte LiDAR — génération de zones")
|
||||
|
||||
# Visibilité de la configuration au démarrage (docker compose logs) : le
|
||||
# backend de génération est TOUJOURS défini par variable d'environnement.
|
||||
logger = logging.getLogger("lidar")
|
||||
if not logger.handlers:
|
||||
# Hors cli.py (uvicorn seul), le logger 'lidar' n'a aucun handler — en
|
||||
# ajouter un pour que la config soit visible dans les logs du conteneur.
|
||||
_h = logging.StreamHandler(sys.stdout)
|
||||
_h.setFormatter(logging.Formatter("%(message)s"))
|
||||
logger.addHandler(_h)
|
||||
logger.setLevel(logging.INFO)
|
||||
if GENERATION_URL:
|
||||
logger.info(f"Backend de génération des tuiles : {GENERATION_URL} "
|
||||
f"(LIDAR_GENERATION_URL)")
|
||||
else:
|
||||
logger.info("Backend de génération des tuiles : local — consultation "
|
||||
"autonome du cache, sans LIDAR_GENERATION_URL")
|
||||
if SYNC_CMD:
|
||||
logger.info(f"Cache local des tuiles : LIDAR_SYNC_CMD"
|
||||
+ (f" (rafraîchi toutes les {AUTO_SYNC_SECONDS:g}s)"
|
||||
if AUTO_SYNC_SECONDS > 0 else " (à la demande)"))
|
||||
|
||||
# assets/ (CSS/JS de l'interface) est créé dès le démarrage pour que le monteur
|
||||
# statique soit actif même avant la première génération de l'index.
|
||||
_assets_dir = OUTPUT_DIR / "assets"
|
||||
@ -116,6 +149,51 @@ def _require_token(x_lidar_token: str = Header(None)):
|
||||
raise HTTPException(401, "token d'API manquant ou invalide")
|
||||
|
||||
|
||||
def _ip_in_regen_cidr(ip):
|
||||
"""Vrai si l'IP appartient au CIDR autorisé (LIDAR_REGEN_CIDR)."""
|
||||
if not REGEN_CIDR:
|
||||
return True # restriction désactivée
|
||||
if not ip:
|
||||
return False
|
||||
import ipaddress
|
||||
try:
|
||||
return ipaddress.ip_address(ip) in ipaddress.ip_network(REGEN_CIDR, strict=False)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _client_ip(request):
|
||||
"""IP d'origine du client pour la restriction de génération.
|
||||
|
||||
Le pair direct doit être dans le réseau autorisé ; dans ce cas seulement,
|
||||
X-Forwarded-For (reverse proxy local) désigne le client réel derrière lui.
|
||||
Un client externe connecté en direct est refusé sur son IP de connexion,
|
||||
quel que soit le X-Forwarded-For qu'il annonce.
|
||||
"""
|
||||
if request is None:
|
||||
return None
|
||||
peer = getattr(request, "client", None)
|
||||
peer_ip = getattr(peer, "host", None) if peer else None
|
||||
if not _ip_in_regen_cidr(peer_ip):
|
||||
return peer_ip # hors réseau (ou inconnu) : jugé sur cette IP
|
||||
xff = request.headers.get("x-forwarded-for") if hasattr(request, "headers") else None
|
||||
if xff:
|
||||
first = xff.split(",")[0].strip()
|
||||
if first:
|
||||
return first
|
||||
return peer_ip
|
||||
|
||||
|
||||
def _require_lan_for_generation(request: Request):
|
||||
"""Réserve /api/generate aux clients du réseau local (LIDAR_REGEN_CIDR)."""
|
||||
ip = _client_ip(request)
|
||||
if not _ip_in_regen_cidr(ip):
|
||||
raise HTTPException(
|
||||
403, f"génération de tuiles réservée au réseau local"
|
||||
f"{f' ({REGEN_CIDR})' if REGEN_CIDR else ''} — "
|
||||
f"client : {ip or 'IP inconnue'}")
|
||||
|
||||
|
||||
def _proxy_api(method, path, payload=None, timeout=30):
|
||||
"""Transmet un appel d'API à la machine de traitement (mode webapp légère).
|
||||
|
||||
@ -365,11 +443,15 @@ def root():
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
def status():
|
||||
def status(request: Request = None):
|
||||
# Droit de lancer une génération depuis cette IP (guide l'interface)
|
||||
regen_allowed = _ip_in_regen_cidr(_client_ip(request))
|
||||
if GENERATION_URL:
|
||||
# Webapp légère : l'état vient de la machine de traitement
|
||||
# Webapp légère : l'état vient de la machine de traitement ; le droit
|
||||
# de génération reste jugé sur l'IP du navigateur (localement).
|
||||
data = _proxy_api("GET", "/api/status", timeout=10)
|
||||
data["distant"] = True
|
||||
data["regen_allowed"] = regen_allowed
|
||||
return data
|
||||
from .progress import progress_snapshot
|
||||
with _job_lock:
|
||||
@ -381,6 +463,8 @@ def status():
|
||||
"returncode": _job["returncode"],
|
||||
"cmd": _job["cmd"],
|
||||
"log": _tail_log(40),
|
||||
# L'interface masque les boutons de génération hors réseau local
|
||||
"regen_allowed": regen_allowed,
|
||||
# Tuiles une par une (nom court, type, état) — agrégées depuis les
|
||||
# événements écrits par le pipeline (.generation.events.jsonl)
|
||||
"tiles": progress_snapshot(OUTPUT_DIR, viz_labels=_viz_step_labels()),
|
||||
@ -475,6 +559,36 @@ def _start_rebuild(with_sync):
|
||||
return {"demarré": True, "sync": bool(with_sync and SYNC_CMD)}
|
||||
|
||||
|
||||
def _auto_sync_once():
|
||||
"""Un cycle de cache local : synchronisation + rebuild, si rien ne tourne.
|
||||
|
||||
Retourne True si un cycle a été lancé, False si un rebuild était déjà en
|
||||
cours (le prochain cycle rattrapera).
|
||||
"""
|
||||
if _rebuild["running"]:
|
||||
return False
|
||||
try:
|
||||
_start_rebuild(with_sync=True)
|
||||
return True
|
||||
except HTTPException:
|
||||
return False # course perdue contre un lancement manuel : tant pis
|
||||
|
||||
|
||||
def _auto_sync_loop():
|
||||
"""Boucle du cache local : synchronise les tuiles périodiquement."""
|
||||
while True:
|
||||
time.sleep(AUTO_SYNC_SECONDS)
|
||||
_auto_sync_once()
|
||||
|
||||
|
||||
# Conteneur webapp seul (cache local des tuiles) : lancer la boucle de
|
||||
# synchronisation en arrière-plan si demandé. Sans LIDAR_SYNC_CMD il n'y a
|
||||
# rien à synchroniser — pas de boucle (juste le rebuild à la demande).
|
||||
if AUTO_SYNC_SECONDS > 0 and SYNC_CMD:
|
||||
threading.Thread(target=_auto_sync_loop, daemon=True,
|
||||
name="cache-tuiles").start()
|
||||
|
||||
|
||||
@app.post("/api/sync", dependencies=[Depends(_require_token)])
|
||||
def sync_and_rebuild():
|
||||
"""Synchronise les tuiles (LIDAR_SYNC_CMD) puis régénère la carte en arrière-plan.
|
||||
@ -578,7 +692,8 @@ def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False
|
||||
return cmd
|
||||
|
||||
|
||||
@app.post("/api/generate", dependencies=[Depends(_require_token)])
|
||||
@app.post("/api/generate", dependencies=[Depends(_require_token),
|
||||
Depends(_require_lan_for_generation)])
|
||||
def generate(req: GenerateRequest):
|
||||
if GENERATION_URL:
|
||||
# Webapp légère : téléchargement + traitement sur la machine distante,
|
||||
|
||||
79
run.sh
79
run.sh
@ -21,6 +21,11 @@
|
||||
# --skip VIZ... Exclure ces visualisations
|
||||
# --file NOM... Traiter un ou plusieurs fichiers LAZ
|
||||
# --test Exécuter les tests unitaires
|
||||
# --serve [PORT] Carte + API de génération (image complète, défaut: 8973)
|
||||
# --serve-webapp [PORT]
|
||||
# Conteneur webapp seul (image légère) : carte servie depuis
|
||||
# le cache local de tuiles, génération déléguée à la machine
|
||||
# de traitement (LIDAR_GENERATION_URL, LIDAR_SYNC_CMD)
|
||||
# -h Afficher l'aide complète
|
||||
|
||||
set -e
|
||||
@ -101,6 +106,9 @@ NO_INDEX_FLAG=""
|
||||
FETCH_TILES_ARGS=""
|
||||
SERVE_FLAG=0
|
||||
SERVE_PORT=8973
|
||||
WEBAPP_FLAG=0
|
||||
WEBAPP_SERVE_PORT=8973
|
||||
WEBAPP_IMAGE="lidar-webapp"
|
||||
|
||||
# Parse arguments manually (more robust than getopts for mixed short/long options)
|
||||
while [ $# -gt 0 ]; do
|
||||
@ -142,6 +150,10 @@ while [ $# -gt 0 ]; do
|
||||
SERVE_FLAG=1; shift
|
||||
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then SERVE_PORT="$1"; shift; fi
|
||||
;;
|
||||
--serve-webapp)
|
||||
WEBAPP_FLAG=1; shift
|
||||
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then WEBAPP_SERVE_PORT="$1"; shift; fi
|
||||
;;
|
||||
-h|--help|-help)
|
||||
echo "Pipeline LiDAR Archéologique"
|
||||
echo ""
|
||||
@ -171,6 +183,11 @@ while [ $# -gt 0 ]; do
|
||||
echo " --fetch-tiles COL,ROW..."
|
||||
echo " Télécharger ces dalles depuis l'IGN puis les traiter"
|
||||
echo " --serve [PORT] Servir la carte + API de génération de zones (défaut: 8973)"
|
||||
echo " --serve-webapp [PORT]"
|
||||
echo " Conteneur webapp seul (image légère, sans PDAL/GPU) :"
|
||||
echo " carte servie depuis le cache local de tuiles, génération"
|
||||
echo " déléguée via LIDAR_GENERATION_URL + LIDAR_SYNC_CMD"
|
||||
echo " (cf. docs/DEPLOY_WEBAPP.md)"
|
||||
echo " --test Exécuter les tests unitaires"
|
||||
echo " -h Afficher cette aide"
|
||||
echo ""
|
||||
@ -224,6 +241,16 @@ build_image() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Image webapp légère (conteneur webapp seul — cf. Dockerfile.webapp)
|
||||
build_webapp_image() {
|
||||
echo "Mise à jour de l'image Docker webapp (cache)..."
|
||||
if ! docker build -f Dockerfile.webapp -t "$WEBAPP_IMAGE" "$SCRIPT_DIR" >/tmp/lidar_webapp_build.log 2>&1; then
|
||||
cat /tmp/lidar_webapp_build.log >&2
|
||||
echo "Échec du build de l'image webapp (journal : /tmp/lidar_webapp_build.log)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for --test flag first
|
||||
if [ "$TEST_FLAG" -eq 1 ]; then
|
||||
build_image
|
||||
@ -255,17 +282,67 @@ if [ "$SERVE_FLAG" -eq 1 ]; then
|
||||
if [ -n "$GPU_ARG" ] && [ "$GPU_ARG" != "all" ]; then
|
||||
GPU_ENV_FLAG="-e LIDAR_GPU=1 -e CUDA_VISIBLE_DEVICES=${GPU_ARG} --gpus '""device=${GPU_ARG}""'"
|
||||
fi
|
||||
# Réseau autorisé à lancer les générations (/api/generate) : la valeur
|
||||
# vient de l'environnement hôte LIDAR_REGEN_CIDR (défaut 192.168.1.0/24,
|
||||
# chaîne vide pour lever la restriction).
|
||||
REGEN_ENV_FLAG="-e LIDAR_REGEN_CIDR=${LIDAR_REGEN_CIDR-192.168.1.0/24}"
|
||||
exec docker run --rm --init $GPU_FLAG $CUDA_ENV_FLAG \
|
||||
--user 1000:1000 \
|
||||
-p "${SERVE_PORT}:8973" \
|
||||
-v "${INPUT_DIR}:/data/input" \
|
||||
-v "${OUTPUT_DIR}:/data/output" \
|
||||
-e LIDAR_INPUT_DIR=/data/input -e LIDAR_OUTPUT_DIR=/data/output \
|
||||
$GPU_ENV_FLAG \
|
||||
$GPU_ENV_FLAG $REGEN_ENV_FLAG \
|
||||
"$IMAGE_NAME" \
|
||||
python3 -m uvicorn lidar_pipeline.webapp:app --host 0.0.0.0 --port 8973
|
||||
fi
|
||||
|
||||
# Mode --serve-webapp : conteneur webapp SEUL (image légère, sans PDAL/GPU).
|
||||
# La carte est servie depuis le cache local de tuiles (output/) ; la
|
||||
# génération est déléguée à la machine de traitement et les tuiles reviennent
|
||||
# par LIDAR_SYNC_CMD (rsync), rafraîchies aussi par LIDAR_AUTO_SYNC_SECONDS.
|
||||
# Variables passées telles quelles depuis l'environnement hôte :
|
||||
# LIDAR_GENERATION_URL machine de traitement (ex: http://192.168.1.50:8973)
|
||||
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
|
||||
# LIDAR_SYNC_CMD rsync qui remplit le cache local de tuiles
|
||||
# LIDAR_AUTO_SYNC_SECONDS resynchronisation périodique (0 = off)
|
||||
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
|
||||
# (défaut 192.168.1.0/24, vide = restriction levée)
|
||||
if [ "$WEBAPP_FLAG" -eq 1 ]; then
|
||||
build_webapp_image
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
AUTO_SYNC_INFO="${LIDAR_AUTO_SYNC_SECONDS:-0}"
|
||||
echo "============================================"
|
||||
echo " Carte LiDAR — webapp seule (image légère)"
|
||||
echo "============================================"
|
||||
echo " http://127.0.0.1:${WEBAPP_SERVE_PORT}/"
|
||||
echo " Génération distante : ${LIDAR_GENERATION_URL:-non configurée (LIDAR_GENERATION_URL)}"
|
||||
if [ -n "$LIDAR_SYNC_CMD" ]; then
|
||||
echo " Cache local tuiles : LIDAR_SYNC_CMD$([ "${LIDAR_AUTO_SYNC_SECONDS:-0}" != "0" ] && echo " (auto toutes les ${AUTO_SYNC_INFO}s)")"
|
||||
else
|
||||
echo " Cache local tuiles : désactivé (définir LIDAR_SYNC_CMD pour rsync)"
|
||||
fi
|
||||
echo "============================================"
|
||||
WEBAPP_ENV=()
|
||||
[ -n "$LIDAR_GENERATION_URL" ] && WEBAPP_ENV+=(-e LIDAR_GENERATION_URL="$LIDAR_GENERATION_URL")
|
||||
[ -n "$LIDAR_REMOTE_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
|
||||
[ -n "$LIDAR_API_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_API_TOKEN="$LIDAR_API_TOKEN")
|
||||
[ -n "$LIDAR_SYNC_CMD" ] && WEBAPP_ENV+=(-e LIDAR_SYNC_CMD="$LIDAR_SYNC_CMD")
|
||||
[ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ] && WEBAPP_ENV+=(-e LIDAR_AUTO_SYNC_SECONDS="$LIDAR_AUTO_SYNC_SECONDS")
|
||||
[ -n "${LIDAR_REGEN_CIDR:-}" ] && WEBAPP_ENV+=(-e LIDAR_REGEN_CIDR="$LIDAR_REGEN_CIDR")
|
||||
# Clé SSH du rsync (LIDAR_SYNC_CMD) : montée en lecture seule si présente
|
||||
WEBAPP_SSH=()
|
||||
[ -d "${HOME}/.ssh" ] && WEBAPP_SSH+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
|
||||
exec docker run --rm --init \
|
||||
--user 1000:1000 \
|
||||
-p "${WEBAPP_SERVE_PORT}:8973" \
|
||||
-v "${OUTPUT_DIR}:/data/output" \
|
||||
"${WEBAPP_SSH[@]}" \
|
||||
-e LIDAR_OUTPUT_DIR=/data/output \
|
||||
"${WEBAPP_ENV[@]}" \
|
||||
"$WEBAPP_IMAGE"
|
||||
fi
|
||||
|
||||
# Lancer le pipeline
|
||||
echo "============================================"
|
||||
echo " Pipeline LiDAR Archéologique"
|
||||
|
||||
5
setup.py
5
setup.py
@ -6,6 +6,11 @@ setup(
|
||||
version='2.0.0',
|
||||
description='Pipeline LiDAR pour détection archéologique',
|
||||
packages=find_packages(),
|
||||
package_data={'lidar_pipeline': [
|
||||
'assets/vendor/*',
|
||||
'assets/vendor/leaflet/*',
|
||||
'assets/vendor/leaflet/images/*',
|
||||
]},
|
||||
python_requires='>=3.8',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
||||
Reference in New Issue
Block a user