From d736336b421f6b792afcded6b40953a988732681 Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Sun, 6 Sep 2026 18:03:29 -0500 Subject: [PATCH 1/4] cleanup derivative queueing buttons and status --- .../src/components/base/Link.svelte | 7 +- .../components/map/MosaicDerivatives.svelte | 138 ++++++++---------- .../shared/tags/MosaicStatus.svelte | 26 ++++ 3 files changed, 94 insertions(+), 77 deletions(-) create mode 100644 ohmg/frontend/svelte_components/src/components/shared/tags/MosaicStatus.svelte diff --git a/ohmg/frontend/svelte_components/src/components/base/Link.svelte b/ohmg/frontend/svelte_components/src/components/base/Link.svelte index 95b98ae0..9b80e315 100644 --- a/ohmg/frontend/svelte_components/src/components/base/Link.svelte +++ b/ohmg/frontend/svelte_components/src/components/base/Link.svelte @@ -9,18 +9,19 @@ export let download = false; export let title = ''; export let classes = []; + export let style = ''; const classStr = classes.join(' '); {#if external || rightArrow || download} {#if download } - + {:else} - + {#if external} @@ -30,7 +31,7 @@ {/if} {:else} - + {/if} diff --git a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte index 811a5965..0f3861e0 100644 --- a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte +++ b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte @@ -8,10 +8,15 @@ import ModalInfo from '../base/ModalInfo.svelte'; import LoadingEllipsis from '../shared/LoadingEllipsis.svelte'; import DerivativeDD from '../shared/DerivativeDD.svelte'; + import MosaicStatus from '../shared/tags/MosaicStatus.svelte'; + import ArrowsClockwise from 'phosphor-svelte/lib/ArrowsClockwise'; export let CONTEXT; export let mapId; + console.log(CONTEXT) + console.log( CONTEXT.user.perms.includes("core.queue_mosaic_cog")) + let layersets = [] let loading = false; @@ -49,52 +54,29 @@ i.masksDateDisplay = i.multimask_date ? new Date(i.multimask_date*1000).toLocaleString() : null; i.xyzStaticArchiveURL = i.xyz_tiles_url ? `${i.xyz_tiles_url}/archive.tar.gz` : null; i.xyzStaticTilesURL = i.xyz_tiles_url ? `${i.xyz_tiles_url}/{z}/{x}/{y}.png` : null; + + i.enableCogQueueBtn = CONTEXT.user.perms.includes("core.queue_mosaic_cog") + i.cogQueueBtnTitle = CONTEXT.user.perms.includes("core.queue_mosaic_cog") ? + "Click to queue COG build" : "You do not have permission for this action" + i.cogStale = false; - i.cogDateDisplay = "---"; - i.showCogQueueBtn = false; - if (i.latest_cog_job) { - if (i.latest_cog_job.stage == "completed") { - i.cogDate = new Date(i.latest_cog_job.date_started * 1000).toLocaleString(); - i.cogDateDisplay = `last updated: ${i.cogDate}`; - i.cogStale = i.multimask_date ? i.latest_cog_job.date_started < i.multimask_date : false - i.showCogQueueBtn = i.cogStale; - } else { - i.cogDateDisplay = i.latest_cog_job.stage; - if (i.latest_cog_job.stage != "queued" && i.latest_cog_job.stage != "running") { - i.showCogQueueBtn = true; - } - } - } else { - i.showCogQueueBtn = true; - i.cogDateDisplay = "not generated"; + if (i.latest_cog_job && i.latest_cog_job.stage == "completed") { + i.cogStale = i.multimask_date ? i.latest_cog_job.date_started < i.multimask_date : false } // same thing now but for the XYZ tileset build - i.showXyzQueueBtn = false; - i.enableXyzQueue = false; - if (i.mosaic_cog_url && !i.cogStale) { - i.enableXyzQueue = true + i.xyzQueueBtnTitle = "Click to queue XYZ tileset build"; + i.enableXyzQueue = true; + if (!i.latest_cog_job || i.cogStale) { + i.enableXyzQueue = false; + i.xyzQueueBtnTitle = "COG must be up-to-date before tileset can be built"; } - - i.xyzStale = false; - i.xyzDateDisplay = "---" - if (i.latest_xyz_job) { - if (i.latest_xyz_job.stage == "completed") { - i.xyzDate = new Date(i.latest_xyz_job.date_started * 1000).toLocaleString(); - i.xyzDateDisplay = `last updated: ${i.xyzDate}`; - i.xyzStale = i.multimask_date ? i.latest_xyz_job.date_started < i.multimask_date : false; - i.showXyzQueueBtn = i.xyzStale; - } else { - i.xyzDateDisplay = i.latest_xyz_job.stage; - if (i.latest_xyz_job.stage != "queued" && i.latest_xyz_job.stage != "running") { - i.showXyzQueueBtn = true; - i.enableXyzQueue = true; - } - } - } else { - i.showXyzQueueBtn = true; - i.xyzDateDisplay = "not generated" + if (!CONTEXT.user.perms.includes("core.queue_mosaic_xyz")) { + i.enableXyzQueue = false; + i.xyzQueueBtnTitle = "You do not have permission for this action"; } + + return i }); loading = false; @@ -128,12 +110,12 @@ {submitQueueRequest('queue-cog-creation')}} > -

Submit mosaic COG generation to queue?

+

Submit mosaic COG generation to queue? If the current COG is up-to-date, there is no need to rebuild it.

{submitQueueRequest('queue-tileset-creation')}} > -

Submit XYZ tileset generation to queue?

+

Submit XYZ tileset generation to queue? If the current XYZ tileset is up-to-date, there is no need to rebuild it.

@@ -141,7 +123,7 @@ mosaic. We provide access to this mosaic in the form of file downloads, web service endpoints, and direct integrations into other platforms. Read more in the docs

If the MultiMask has been updated after one of these artifacts was generated, dates will be shown in - red and you can queue that mosaic to be rebuilt.

+ red and you can queue that mosaic to be rebuilt.

{#if loading} @@ -152,24 +134,31 @@ {`${ls.name} (${ls.layers_masked_ct}/${ls.layers.length} layers masked)`} - {#if ls.multimask_date} - - masks last updated: {ls.masksDateDisplay} - - {/if} +
+ {#if ls.multimask_date} + + masks last edit: {ls.masksDateDisplay} + + {/if} + +
Cloud Optimized GeoTIFF - - {ls.cogDateDisplay} - {#if ls.showCogQueueBtn} - - {/if} - + }}>build COG +
Direct download (.tif)
Static XYZ Tileset - - {ls.xyzDateDisplay} - {#if ls.showXyzQueueBtn} - - {/if} - + }}>build XYZ tileset +
Direct download (gzipped tarfile)
- Extensions... - - always current, unless noted - + Extensions
Open in OpenHistoricalMap editor (uses XYZ tile endpoint)
\ No newline at end of file diff --git a/ohmg/frontend/svelte_components/src/components/shared/tags/MosaicStatus.svelte b/ohmg/frontend/svelte_components/src/components/shared/tags/MosaicStatus.svelte new file mode 100644 index 00000000..cd78e4c3 --- /dev/null +++ b/ohmg/frontend/svelte_components/src/components/shared/tags/MosaicStatus.svelte @@ -0,0 +1,26 @@ + + +{#if job.stage == "completed"} + {#if stale} +
+ stale +
+ {:else} +
+ up-to-date +
+ {/if} +{:else} +
+ {job.stage} +
+{/if} \ No newline at end of file From a2ca27148cad0de8408c86a3b1cea0202ab462b4 Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Sun, 6 Sep 2026 18:17:15 -0500 Subject: [PATCH 2/4] update wording and derivative names --- .../components/map/MosaicDerivatives.svelte | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte index 0f3861e0..4f070d11 100644 --- a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte +++ b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte @@ -119,11 +119,12 @@
-

Once layers have been trimmed in the MultiMask they can be combined into a single - mosaic. We provide access to this mosaic in the form of file downloads, web service endpoints, and direct integrations - into other platforms. Read more in the docs

-

If the MultiMask has been updated after one of these artifacts was generated, dates will be shown in - red and you can queue that mosaic to be rebuilt.

+

Once layers have been trimmed in the MultiMask they are combined into a single + mosaic output, or "derivative". You can access these derivatives here in the form of file downloads, web service endpoints, and direct integrations + into other platforms.

+

To queue the creation (or recreation) of mosaics for each layerset, use the summary table below. If a derivative has been created, but + the MultiMask has since been edited, it will be marked as stale it should be queued for a rebuild. + Read more in the docs

{#if loading} @@ -172,7 +173,7 @@ linkType="copytext" naMessage="requires COG" /> -
XYZ tile endpoint (dynamic)
+
Dynamic XYZ endpoint
- Static XYZ Tileset + XYZ Tileset
{#if ls.latest_xyz_job} @@ -206,7 +207,7 @@ linkType="download" naMessage="not yet generated" /> -
Tiles endpoint
+
Static XYZ endpoint
Extensions -
Open in OpenHistoricalMap editor (uses XYZ tile endpoint)
+
Open in OpenHistoricalMap editor (uses dynamic XYZ endpoint)
Date: Sun, 6 Sep 2026 18:19:24 -0500 Subject: [PATCH 3/4] remove print statements --- .../src/components/map/MosaicDerivatives.svelte | 3 --- 1 file changed, 3 deletions(-) diff --git a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte index 4f070d11..866006fa 100644 --- a/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte +++ b/ohmg/frontend/svelte_components/src/components/map/MosaicDerivatives.svelte @@ -14,9 +14,6 @@ export let CONTEXT; export let mapId; - console.log(CONTEXT) - console.log( CONTEXT.user.perms.includes("core.queue_mosaic_cog")) - let layersets = [] let loading = false; From a8916b404abf8a364744d3f2fbaf230fe031eef1 Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Tue, 8 Sep 2026 15:54:53 -0500 Subject: [PATCH 4/4] update docs for mosaic generation --- docs/guides/generating-mosaics.md | 65 +++++++++++++++++++---- docs/images/detroit-vol2-1897-cog.png | Bin 0 -> 1166411 bytes docs/images/detroit-vol2-1897-masked.png | Bin 0 -> 1200987 bytes docs/images/mosaic-cog-fresh.png | Bin 18858 -> 0 bytes docs/images/mosaic-cog-queued.png | Bin 0 -> 17397 bytes docs/images/mosaic-cog-running.png | Bin 0 -> 17290 bytes docs/images/mosaic-cog-stale.png | Bin 20403 -> 16973 bytes docs/images/mosaic-cog-up-to-date.png | Bin 0 -> 17425 bytes 8 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 docs/images/detroit-vol2-1897-cog.png create mode 100644 docs/images/detroit-vol2-1897-masked.png delete mode 100644 docs/images/mosaic-cog-fresh.png create mode 100644 docs/images/mosaic-cog-queued.png create mode 100644 docs/images/mosaic-cog-running.png create mode 100644 docs/images/mosaic-cog-up-to-date.png diff --git a/docs/guides/generating-mosaics.md b/docs/guides/generating-mosaics.md index de0efde6..790ebf63 100644 --- a/docs/guides/generating-mosaics.md +++ b/docs/guides/generating-mosaics.md @@ -1,13 +1,34 @@ # Generating mosaics -Once layers are grouped into layersets, and each layerset is trimmed in the [multimask](./trimming.md) interface, the final step is to collapse the content into a single mosaic per layerset, like finally gluing down all the individual clippings in a collage. You'll find these within within a map's **Mosaic** → **Derivatives** (see [New Iberia, 1885](https://oldinsurancemaps.net/map/sanborn03375_001#mosaic)). +## Understanding mosaic "derivatives" + +Once layers are grouped into layersets, and each layerset is trimmed in the [MultiMask](./trimming.md) interface, the final step is to collapse the content into a single mosaic per layerset, like finally glueing down all the individual clippings in a collage. + +![masked detroit 1897 vol. 2](../images/detroit-vol2-1897-masked.png) + +This screenshot of [Detroit, Mich., 1897, vol. 2](https://oldinsurancemaps.net/map/sanborn03985_005), shows 106 individual layers that have been georeferenced and masked. Creating a derivative from this mosaic produces a single layer, in this case a Cloud Optimized GeoTIFF. + +![cog mosaic for detroit 1897 vol. 2](../images/detroit-vol2-1897-cog.png) + +This greatly improves the utility of what you have created by allowing you to do things like: + +- Download a single mosaic file for an entire volume +- Load a single layer of the entire volume into web applications like HistoryForge or OpenHistoricalMap + +It also speeds up parts of OIM that use mosaic derivatives when they are available. + +## Accessing derivatives + +Within within a map's **Mosaic** → **Derivatives** (see [New Iberia, 1885](https://oldinsurancemaps.net/map/sanborn03375_001#mosaic)) section you'll find a list of all "layersets" within that map (usually just Main Content, but other categories could appear here as well if they have more than one layer). Currently, OIM supports the creation of two different derivatives per layerset: - **Cloud Optimized GeoTIFFs (COG)** - This is the primary output, and when present, is used within OIM in certain contexts -- **Static XYZ Tileset** - - This is a rendering of the latest COG mosaic to actual PNG tiles in a {z}/{x}/{y}.png folder structure. +- **XYZ Tileset** + - This is a rendering of the latest COG mosaic to actual PNG tiles in a {z}/{x}/{y}.png folder structure + +These two formats, in turn, allow us to provide other formats and web services as well. !!! note @@ -15,14 +36,40 @@ Currently, OIM supports the creation of two different derivatives per layerset: ## Keeping derivatives up-to-date -The creation of a single geotiff (or static XYZ tileset) is effectively a snapshot of the work that has been performed on a map's layers and masks up to that point. Timestamps are displayed showing when masks were last updated, and when derivatives were last generated. +The creation of a single geotiff (or static XYZ tileset) is effectively a snapshot of the work that has been performed on a map's layers and masks up to that point. The derivatives list will show a timestamp for the last time an edit was made to the multimask, as well as a small status tag for each format showing the status of its last mosaic job. + +### `up-to-date` + +![up-to-date cog derivative](../images/mosaic-cog-up-to-date.png) + +A derivative is **up-to-date** when there has been no updates to the multimask or layers since it was created. You can hover the tag to see when it was last run. + + +### `stale` + +![stale cog derivative](../images/mosaic-cog-stale.png) + +If a derivative is marked as **stale**, that means that the masks or layers used to create it have been changed since the last job was run. In this case you can click the **build COG** button to queue the re-creation of this derivative. + +!!! note + + Because **XYZ Tilesets** are generated _from_ COGs, the COG derivative must be up-to-date, or queued, before you will be able to queue the creation of a tileset. + +### `queued` + +![queued cog derivative](../images/mosaic-cog-queued.png) + +Because the process to create a COG (or XYZ tileset) from dozens or hundreds of layers can take several hours, we use a **queueing** system to ensure the server is never overloaded. Queued mosaic operations are processed with a first in, first out policy. You can see the status of mosaicking processes (queued, running, or completed) in [Jobs](https://oldinsurancemaps.net/jobs). + +### `running` -![up-to-date cog derivative with timestamps](../images/mosaic-cog-fresh.png) +![queued cog derivative](../images/mosaic-cog-running.png) -However, this means that if the ground control points are subsequently updated for a layer, or the masks are adjusted, the derivative artifact will be out-of-date. In this case, the interface will show timestamps of out-of-date artifacts in red and a button will be available allowing you to queue this artifact to be rebuilt. +Once a job has been picked up by a runner, it will change from queued to **running** for as long as the process takes, eventually returning to **up-to-date** when the process is completed. -![out-of-date cog derivative with red timestamp](../images/mosaic-cog-stale.png) +## How long does it take for a job to complete? -Because the process to create a single geotiff (or xyz tileset) from dozens or hundreds of layers can take several hours, we use a queueing system to ensure the server is never overloaded. You can see all mosaicking processes (queued, running, or completed) in [Jobs](https://oldinsurancemaps.net/jobs). +This depends on the layerset itself, and which format of derivative you are creating. -In the current setup, only two jobs are allowed to run at any time, and queued mosaic operations are processed with a first in, first out policy. +- COG: The greater the **geographic extent** of all layers (imagine drawing a big rectangle that encompasses **all** of the layers in the layerset), the longer the job (and larger the resulting file). +- XYZ Tileset: The greater the **geographic coverage** of all layers in the mosaic (imagine all of the mask boundaries dissolved into a big polygon, or multi-polygon), the longer the job (and higher the number of tiles in the tileset). diff --git a/docs/images/detroit-vol2-1897-cog.png b/docs/images/detroit-vol2-1897-cog.png new file mode 100644 index 0000000000000000000000000000000000000000..9295621c420aefe68cae4415ffd28cdba443ccbc GIT binary patch literal 1166411 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