feat: tile building footprints once per image layer - #183
Open
Caleb Robinson (calebrob6) wants to merge 2 commits into
Open
feat: tile building footprints once per image layer#183Caleb Robinson (calebrob6) wants to merge 2 commits into
Caleb Robinson (calebrob6) wants to merge 2 commits into
Conversation
Footprint geometry belongs to the image layer: every model trained on a layer draws the same buildings. The embedding job tiled them anyway, once per model, and standard layers got no tiles at all. Imagery prep now queues a tiling job as soon as a layer's footprints are cached, for both workflow types, and the archive lands on ImageLayer.footprintPmtilesUrl. The standard labeling workflow does not read it yet; this only produces the artifact. The embedding job stops tiling. tippecanoe and subprocess drop out of embed_buildings entirely and it emits the embeddings GeoJSON, the HFTR feature sidecar and a manifest. The feature sidecar is genuinely per model and is unchanged. The two archives were the same file. Both ran tippecanoe with the same layer name, the same --use-attribute-for-id=id, the same -y id -y overture_id and the same zoom range, over the same rows in the same order (embed_buildings reads the layer footprints, reset_index, and keeps every row including the ones with no valid tokens, which is what the positional prediction join already depends on). So an embedding layer was paying for a duplicate. tippecanoe ships only in the training image, and it is not in the apt repos of the imageryprep base image, so tiling runs as a queued task in the training container through the existing UnifiedRunner rather than inline in imagery prep. The queue is named footprint-tiles-queue rather than for any one consumer, since it tiles layers for every workflow. With one archive, Model.pmtilesUrl and ArtifactTypes.BUILDING_PMTILES retire, GetModelArtifact serves footprint_pmtiles as a layer-scoped kind instead of a per-model one, and the Interactive Labeler reads that. Existing layers have no archive and existing embedding models point at a per-model one that nothing writes any more. Re-tiling them is out of scope; the queued job means it can be done later by enqueuing one message per layer, without re-running imagery prep. The labeler used to refuse to start when a model had no archive. That guard goes with the field, so a failed tile load now raises a clear error rather than warning to the console and leaving an empty map with no buildings to label.
GetModelArtifact required modelId for every kind, including the layer-scoped footprint tiles. A standard-workflow layer can have no models at all, which made its own archive unfetchable. modelId is now optional when the kind is layer-scoped and an explicit imageLayerId is given. Every model-scoped kind still requires it, and a request carrying a modelId still resolves through that model's layer, so the Interactive Labeler is unaffected.
Copilot started reviewing on behalf of
Caleb Robinson (calebrob6)
August 31, 2026 23:06
View session
Contributor
There was a problem hiding this comment.
Pull request overview
Moves building-footprint PMTiles generation from per-model embedding to a shared, image-layer-scoped queued workflow.
Changes:
- Adds footprint tiling workflow, processor, queue trigger, state, and configuration.
- Removes PMTiles generation and ownership from embedding models.
- Updates artifact retrieval and the Interactive Labeler to use layer-scoped tiles.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
ui/src/Components/InteractiveLabeler/InteractiveLabeler.jsx |
Loads shared layer footprint tiles. |
local.settings.example.jsonc |
Documents the new queue setting. |
hastelib/tests/workflows/test_prepare_footprint_tiles.py |
Tests tiling conversion and naming. |
hastelib/tests/core/processors/test_imagery_footprint_tiles.py |
Tests imagery-triggered queueing. |
hastelib/tests/core/processors/test_footprint_tiles.py |
Tests preparation and queue decisions. |
hastelib/src/hastegeo/workflows/prepare_footprint_tiles.py |
Implements PMTiles generation. |
hastelib/src/hastegeo/workflows/embed_buildings.py |
Removes per-model tiling. |
hastelib/src/hastegeo/core/processors/imagery.py |
Queues tiling after imagery preparation. |
hastelib/src/hastegeo/core/processors/footprint_tiles.py |
Adds the tiling state machine. |
hastelib/src/hastegeo/core/processors/embedding.py |
Removes PMTiles embedding outputs. |
hastelib/src/hastegeo/core/models/projects.py |
Moves tile state onto ImageLayer. |
hastelib/src/hastegeo/core/config.py |
Adds artifact, queue, and metadata types. |
docker/training/Dockerfile |
Adds the tiling CLI shim. |
docker/docker-compose.yml |
Configures the local queue. |
docker/data-init/upload_data.py |
Creates the local queue. |
api/hastefuncqueues/function_app.py |
Adds the queue trigger. |
api/hastefuncapi/function_app.py |
Serves layer-scoped footprint tiles. |
Suppressed comments (2)
hastelib/src/hastegeo/core/processors/footprint_tiles.py:451
- This reads the manifest only from the Batch node. Autoscale nodes can be deallocated as soon as the task completes, and
AzureBatchRunner.get_filecontent_from_task()then returns no content; unlikeImageryPostProcessor._read_task_output(), there is no fallback to the copy uploaded under the task output prefix, so a successful tiling task is marked failed. Read the uploaded blob copy when the node-local read misses.
content = self.runner.get_filecontent_from_task(
job_id=job.jobId,
task_id=job.taskId,
filename=MANIFEST_FILENAME,
)
api/hastefuncqueues/function_app.py:695
- Swallowing an unexpected exception makes Azure Functions acknowledge and delete the queue message. Transient metadata/storage failures therefore strand the layer without a retry; log the failure and re-raise so the queue runtime can retry and eventually poison the message.
except Exception as e:
logger.error(
"GetPrepareFootprintTilesQueueTrigger: Error processing queue "
f"message: {e}\n{traceback.format_exc()}",
stack_info=True,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+83
to
+88
| return { | ||
| "projectId": project_id, | ||
| "imageLayerId": image_layer_id, | ||
| "sourceFootprintsUrl": source_footprints_url or "", | ||
| "force": bool(force), | ||
| } |
Comment on lines
+608
to
+613
| enqueue_footprint_tiles( | ||
| project_id=self.image_data.projectId, | ||
| image_layer_id=self.image_data.imageLayerId, | ||
| source_footprints_url=self.image_data.buildingFootprintsUrl, | ||
| config=self.config, | ||
| ) |
Comment on lines
+669
to
+670
| output = await asyncio.to_thread( | ||
| FootprintTilesPreprocessor(image_layer).process |
Comment on lines
+837
to
+842
| console.error("Failed to load the footprint PMTiles archive:", e); | ||
| throw new Error( | ||
| "The building footprint tiles for this image layer are not ready " + | ||
| "yet. They are built once per layer, shortly after the layer " + | ||
| "finishes processing. Try again in a few minutes." | ||
| ); |
RC artifacts readyAll branch deployment references use the same RC tag:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Building footprints get tiled twice for a building-workflow layer, and not at all for a standard one. This tiles them once, when the layer's footprints are cached, and has the embedding job reuse that archive.
What changed
Imagery prep queues a tiling job as soon as a layer's building footprints are cached, for both
workflowTypevalues. The archive lands onImageLayer.footprintPmtilesUrl. The standard labeling workflow does not read it yet — this only produces the artifact.The embedding job stops tiling.
tippecanoeandsubprocessdrop out ofembed_buildings.pyentirely; it emits the embeddings GeoJSON, the HFTR feature sidecar and a manifest. The feature sidecar is genuinely per model and is unchanged.Why the two archives were the same file
Both tippecanoe invocations already used the same layer name (
buildings), the same--use-attribute-for-id=id, the same-y id -y overture_idand the same zoom range.embed_buildingsreads the layer footprints, doesreset_index(drop=True)and keeps every row — including buildings outside the raster, which stay as NaN features — soid = 0..N-1means the same thing in both. That is the invariant the positional prediction join already depends on. A second embedding model on the same layer produced a second identical archive.Where the tiling runs
tippecanoeships only in the training image, via conda-forge. I checked the imageryprep base image (mcr.microsoft.com/azure-functions/python:4-nightly-python3.11-slim) and tippecanoe is not in its apt repos, so tiling inline in the existing imagery-prep job would have meant building it from source in a second image. Instead the work runs as a queued task in the training container through the existingUnifiedRunner.The queue is
footprint-tiles-queue, named for the work rather than any one consumer, since it tiles layers for every workflow.Enqueuing is best-effort: the tiles are derived data, so an unreachable queue leaves the imagery perfectly usable and the job can be re-requested later without re-running imagery prep.
Knock-on simplifications
With one archive,
Model.pmtilesUrlandArtifactTypes.BUILDING_PMTILESretire, andGetModelArtifactservesfootprint_pmtilesas a layer-scoped kind instead of a per-model one. The Interactive Labeler reads that.modelIdis now optional onGetModelArtifactwhen the kind is layer-scoped and an explicitimageLayerIdis given. A standard-workflow layer can have no models at all, which would otherwise have made its own archive unfetchable. Every model-scoped kind still requires it.The labeler used to refuse to start when a model had no archive. That guard goes with the field, so a failed tile load now raises a clear error rather than warning to the console and leaving an empty map with no buildings to label.
Verification
hatch run test:pytestnode --test)vite buildmainin the same containerblack/isort/flake8All 47 Python failures also fail on
main; the sets were diffed rather than compared by count. Forty-six are thehastegeo[planetary-computer]extra missing from the local conda environment, which CI installs, and the last is the pre-existingtest_artifacts.py::test_zip. 22 of the 414 passing tests are new.Exercised on the local docker stack, tiling two real layers end to end through queue, trigger, processor, runner and tippecanoe:
Processedin about 40 secondsimageLayerIdalone for both layers, and bymodelIdfor a model on one of them, returning the identical byteskind=pmtilesnow 400s, and the per-modelkind=sidecarstill returns its 1.4 MB payloadembed_buildingsno longer referenceswrite_pmtiles,tippecanoeorsubprocess, and the embedding job config no longer names a pmtiles outputThe first live run failed usefully: the training image predated the new workflow module, so the task died with
ModuleNotFoundError. That is worth knowing for deployment — the training image has to ship with this change, since it carries both the workflow and the newprepare-footprint-tilesshim.Known limitations
Existing layers have no archive, and existing embedding models point at a per-model one that nothing writes any more. Re-tiling them is out of scope by agreement. The queued job means it can be done later by enqueuing one message per layer, without re-running imagery prep.
A layer whose footprints never cached still cannot be tiled. Imagery prep already flips those layers to FAILED, so the enqueue is skipped.
Bicep app-setting parity is skipped for the new queue, matching the seven existing ones.
Configresolves the default and the Functions host creates the queue on first use; editingfunctions.bicepwithout regeneratinginfra/main.jsoncauses drift.No browser tests. This repo has no Playwright config, so the labeler change was verified through the API, the rendered bundle and lint rather than by loading the page.