Skip to content

Commit aa9df50

Browse files
authored
Feature: add collection render layers to map in HTML collection view (#40)
* add tilejson and map links to collection if titiler_endpoint is set * add render layers to the html collection view * fix direct links to stac browser
1 parent acdd687 commit aa9df50

File tree

4 files changed

+116
-7
lines changed

4 files changed

+116
-7
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ services:
8989
- MOSAIC_CONCURRENCY=1
9090
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
9191
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
92+
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
9293
env_file:
9394
- path: .env
9495
required: false

infrastructure/app.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import yaml
55
from aws_cdk import (
66
App,
7+
Duration,
78
RemovalPolicy,
89
Stack,
910
aws_certificatemanager,
@@ -387,11 +388,18 @@ def __init__(
387388
),
388389
default_root_object="index.html",
389390
error_responses=[
391+
aws_cloudfront.ErrorResponse(
392+
http_status=403,
393+
response_http_status=200,
394+
response_page_path="/index.html",
395+
ttl=Duration.seconds(0),
396+
),
390397
aws_cloudfront.ErrorResponse(
391398
http_status=404,
392399
response_http_status=200,
393400
response_page_path="/index.html",
394-
)
401+
ttl=Duration.seconds(0),
402+
),
395403
],
396404
certificate=aws_certificatemanager.Certificate.from_certificate_arn(
397405
self,

runtimes/eoapi/stac/eoapi/stac/client.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
Type,
1515
get_args,
1616
)
17-
from urllib.parse import unquote_plus, urljoin
17+
from urllib.parse import unquote_plus, urlencode, urljoin
1818

1919
import attr
2020
import jinja2
@@ -243,6 +243,32 @@ async def get_queryables(
243243
return queryables
244244

245245

246+
def add_render_links(collection: Collection, titiler_endpoint: str) -> Collection:
247+
"""Adds links to html preview and tilejson endpoints for a collection"""
248+
if renders := collection.get("renders"):
249+
base_url = f"{titiler_endpoint}/collections/{collection['id']}/WebMercatorQuad"
250+
for render, metadata in renders.items():
251+
query_params = urlencode(metadata, doseq=True)
252+
collection["links"].append(
253+
{
254+
"rel": Relations.preview.value,
255+
"title": f"{render} interactive map",
256+
"type": MimeTypes.html.value,
257+
"href": f"{base_url}/map?{query_params}",
258+
}
259+
)
260+
collection["links"].append(
261+
{
262+
"rel": Relations.tiles.value,
263+
"title": f"{render} tiles",
264+
"type": MimeTypes.json.value,
265+
"href": f"{base_url}/tilejson.json?{query_params}",
266+
}
267+
)
268+
269+
return collection
270+
271+
246272
@attr.s
247273
class PgSTACClient(CoreCrudClient):
248274
pgstac_search_model: Type[PgstacSearch] = attr.ib(default=PgstacSearch)
@@ -383,6 +409,9 @@ async def all_collections(
383409
**kwargs,
384410
) -> Collections:
385411
collections = await super().all_collections(request, *args, **kwargs)
412+
if titiler_endpoint := request.app.state.settings.titiler_endpoint:
413+
for collection in collections["collections"]:
414+
collection = add_render_links(collection, titiler_endpoint)
386415

387416
output_type: Optional[MimeTypes]
388417
if f:
@@ -425,6 +454,9 @@ async def get_collection(
425454
collection_id, request, *args, **kwargs
426455
)
427456

457+
if titiler_endpoint := request.app.state.settings.titiler_endpoint:
458+
collection = add_render_links(collection, titiler_endpoint)
459+
428460
output_type: Optional[MimeTypes]
429461
if f:
430462
output_type = MimeTypes[f]

runtimes/eoapi/stac/eoapi/stac/templates/collection.html

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ <h1 class="my-4">
3939
{% for keyword in response.keywords %}
4040
<li class="badge badge-secondary">{{ keyword }}</li>
4141
{% endfor %}
42-
</lul>
42+
</ul>
4343
</div>
4444
{% endif %}
4545

@@ -72,17 +72,33 @@ <h2>Links</h2>
7272
</div>
7373

7474
{% if response.extent and response.extent.spatial %}
75+
76+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
77+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.fullscreen/2.0.0/Control.FullScreen.css" />
78+
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.fullscreen/2.0.0/Control.FullScreen.min.js"></script>
79+
7580
<script>
7681
window.addEventListener("load", function() {
7782
const collection = {{ response|tojson }};
78-
var map = L.map('map').setView([0, 0], 1);
79-
map.addLayer(new L.TileLayer(
83+
var map = L.map('map', {
84+
fullscreenControl: true,
85+
fullscreenControlOptions: {
86+
position: 'bottomright',
87+
title: 'View Fullscreen',
88+
titleCancel: 'Exit Fullscreen',
89+
content: '<i class="fa fa-expand"></i>' // You can customize this icon
90+
}
91+
}).setView([0, 0], 1);
92+
93+
var osmLayer = new L.TileLayer(
8094
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
8195
maxZoom: 18,
8296
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
8397
}
84-
));
98+
);
99+
map.addLayer(osmLayer);
85100

101+
// Draw the bounding boxes
86102
for (let i = 0, len = collection.extent.spatial.bbox.length; i < len; i++) {
87103
const options = i === 0 ? {
88104
fill: false,
@@ -101,12 +117,64 @@ <h2>Links</h2>
101117
[bbox[3], bbox[0]]
102118
], options);
103119

104-
105120
map.addLayer(bbox_polygon);
106121
if (i === 0) {
107122
map.fitBounds(bbox_polygon.getBounds());
108123
}
109124
}
125+
126+
// Add any tilejson links as layers to the map
127+
const tileJsonLinks = collection.links.filter(link => link.rel === "tiles");
128+
129+
const overlayLayers = {};
130+
131+
if (tileJsonLinks.length > 0) {
132+
tileJsonLinks.forEach((link, index) => {
133+
fetch(link.href)
134+
.then(response => response.json())
135+
.then(tileJson => {
136+
console.log(tileJson)
137+
const tileLayer = L.tileLayer(tileJson.tiles[0], {
138+
attribution: tileJson.attribution || '',
139+
minZoom: tileJson.minzoom || 0,
140+
maxZoom: tileJson.maxzoom || 18
141+
});
142+
143+
const layerName = link.title || `TileJSON Layer ${index + 1}`;
144+
overlayLayers[layerName] = tileLayer;
145+
146+
// Add the first layer to the map by default
147+
if (index === 0) {
148+
tileLayer.addTo(map);
149+
}
150+
151+
// Add layer control after all layers are processed
152+
if (index === tileJsonLinks.length - 1) {
153+
// Define the base layers
154+
const baseLayers = {
155+
"OpenStreetMap": osmLayer
156+
};
157+
158+
// Add the layer control to the map
159+
L.control.layers(baseLayers, overlayLayers).addTo(map);
160+
}
161+
})
162+
.catch(error => {
163+
console.error("Error loading TileJSON:", error);
164+
});
165+
});
166+
}
167+
168+
// Handle fullscreen change event to resize map
169+
map.on('fullscreenchange', function() {
170+
if (map.isFullscreen()) {
171+
console.log('Entered fullscreen');
172+
} else {
173+
console.log('Exited fullscreen');
174+
}
175+
// Make sure the map fills the container after fullscreen changes
176+
map.invalidateSize();
177+
});
110178
});
111179
</script>
112180
{% endif %}

0 commit comments

Comments
 (0)