Created
February 4, 2022 22:07
-
-
Save vincentsarago/33810b53dbafc6ec81ea6d1691ec7595 to your computer and use it in GitHub Desktop.
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset='utf-8' /> | |
<title>Simple STAC API Viewer</title> | |
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> | |
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js'></script> | |
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css' rel='stylesheet' /> | |
<link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.0/mapbox-gl-draw.css' type='text/css' /> | |
<script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.0/mapbox-gl-draw.js'></script> | |
<link href='https://api.mapbox.com/mapbox-assembly/v0.23.2/assembly.min.css' rel='stylesheet'> | |
<script src='https://api.mapbox.com/mapbox-assembly/v0.23.2/assembly.js'></script> | |
<script src='https://unpkg.com/[email protected]/builds/index.js'></script> | |
<style> | |
body { margin: 0; padding: 0; width: 100%; height: 100%;} | |
#map { | |
position: fixed; | |
left: 600px; | |
width: calc(100% - 600px); | |
height: 100%; | |
background-color: #000; | |
} | |
#menu { | |
position: fixed; | |
left: 0; | |
width: 600px; | |
height: 100%; | |
-o-transition: all .5s ease; | |
-webkit-transition: all .5s ease; | |
-moz-transition: all .5s ease; | |
-ms-transition: all .5s ease; | |
transition: all ease .5s; | |
background-color: #FFF; | |
} | |
.loading-map { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
color: #FFF; | |
background-color: #000; | |
text-align: center; | |
opacity: 0.5; | |
font-size: 45px; | |
} | |
.loading-map.off{ | |
opacity: 0; | |
-o-transition: all .5s ease; | |
-webkit-transition: all .5s ease; | |
-moz-transition: all .5s ease; | |
-ms-transition: all .5s ease; | |
transition: all ease .5s; | |
visibility:hidden; | |
} | |
.middle-center { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
.middle-center * { | |
display: block; | |
padding: 5px; | |
} | |
#toolbar { | |
height: 35px; | |
} | |
#toolbar li { | |
display: block; | |
color: #fff; | |
background-color: #556671; | |
font-weight: 700; | |
font-size: 12px; | |
padding: 5px; | |
height: 100%; | |
width: 100%; | |
text-transform: uppercase; | |
text-align: center; | |
text-decoration: none; | |
outline: 0; | |
cursor: pointer; | |
-webkit-touch-callout: none; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
} | |
#toolbar li svg { | |
font-size: 25px; | |
line-height: 25px; | |
padding-bottom: 0; | |
} | |
#toolbar li:hover { | |
background-color: #28333b; | |
} | |
#toolbar li.active { | |
color: #000; | |
background-color: #fff; | |
} | |
#toolbar li.disabled { | |
pointer-events:none; | |
opacity:0.4; | |
} | |
#menu-content { | |
height: calc(100% - 35px); | |
} | |
#menu-content section { | |
display: none; | |
position: relative; | |
height: 100%; | |
overflow-y: scroll; | |
} | |
#menu-content section.active { | |
display: inherit; | |
} | |
#results-items { | |
text-align: left; | |
color: #393939; | |
} | |
#results-items .list-element .col{ | |
border-top: 1px solid #bfbfbf; | |
} | |
#results-items .list-element .col { | |
color: #393939; | |
cursor: pointer; | |
} | |
#results-items .list-element .col:hover { | |
background-color: rgb(88, 84, 84); | |
background-color: rgba(88, 84, 84, 0.2); | |
} | |
.item-descr { | |
font-weight: 100; | |
display: inline-block; | |
padding: 3px; | |
} | |
.item-descr .id { | |
display: block; | |
color: #636262; | |
line-height: 18px; | |
font-size: 12px; | |
-webkit-transition: all .5s ease; | |
-moz-transition: all .5s ease; | |
-ms-transition: all .5s ease; | |
transition: all .5s ease; | |
} | |
.item-descr .date { | |
display: block; | |
font-size: 10px; | |
} | |
.mapboxgl-popup { | |
max-width: 400px !important; | |
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; | |
} | |
@media(max-width: 767px) { | |
#map { | |
left: 300px; | |
width: calc(100% - 300px); | |
} | |
#menu { | |
width: 300px; | |
} | |
.mapboxgl-ctrl-attrib { | |
font-size: 10px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div id='selector' class='fixed top right bottom left scroll-auto bg-darken50 z3'> | |
<div class='bg-white middle-center w600 px12 py12 round'> | |
<div class='txt-h5 mt6 mb6 color-black'>Enter STAC API url</div> | |
<input id="stacapi" class='input wmax-full inline-block' value="" placeholder='STAC API url here' /> | |
<button id="launch" class='btn bts--xs btn--stroke bg-darken25-on-hover inline-block mt12 '>Apply</button> | |
</div> | |
</div> | |
<div id='menu' class='bg-white z1'> | |
<ul id='toolbar' class='grid'> | |
<li id='query' class="col col--6 active" title="query" onclick="switchPane(this)"> | |
Query | |
</li> | |
<li id='results' class="col col--6" title="results" onclick="switchPane(this)"> | |
Results | |
</li> | |
</ul> | |
<div id='menu-content' class='relative'> | |
<!-- Query --> | |
<section id='query-section' class='px12 pt12 pb6 active'> | |
<div class='align-center mb12'> | |
<button id='btn-query' class='btn center' title='Submit'>Submit Query</button> | |
<button id='btn-reset' class='btn center' title='Reset'>Reset</button> | |
</div> | |
<div class='align-center mb12'> | |
<button id='bbox' class='btn btn--stroke color-black round center txt-s opacity75-on-hover' title='bbox'>DRAW BBOX ON MAP | |
<svg class='icon w18 h18 inline-block align-middle'><use xlink:href='#icon-polygon'/></svg> | |
</button> | |
</div> | |
<div class='px12 pt12 pb6'> | |
<div class='txt-h5 mt6 mb6 color-black'>Date Range</div> | |
<div class='align-center'> | |
<input id="start-date" class='input input--s wmax120 wmax180-ml inline-block align-center color-black opacity75-on-hover'/> | |
<input id="end-date" class='input input--s wmax120 wmax180-ml inline-block align-center color-black opacity75-on-hover'/> | |
</div> | |
</div> | |
<div class='px12 pt12 pb6'> | |
<div class='txt-h5 mt6 mb6 color-black'>Collection Filter</div> | |
<div id='collection-filters' class='align-center'></div> | |
<div class='align-center mt12'> | |
<button id='add-collection' class='btn btn--s btn--stroke color-black round mt16 wmax180 w300-ml opacity75-on-hover' title='Add'> | |
Add filter | |
</button> | |
</div> | |
</div> | |
<div class='px12 pt12 pb6'> | |
<div class='txt-h5 mt6 mb6 color-black'>Property Filters</div> | |
<div id='property-filters' class='align-center '></div> | |
<div class='align-center mt12'> | |
<button id='add-filter' class='btn btn--s btn--stroke color-black round mt16 wmax180 w300-ml opacity75-on-hover' title='Add'> | |
Add filter | |
</button> | |
</div> | |
</div> | |
</section> | |
<!-- Results --> | |
<section id='results-section'> | |
<div id="results-items" class='px12 pt12 pb6'></div> | |
<div class='align-center mb12'> | |
<button id='btn-load-more' class='btn center none' title='load more'>More Results</button> | |
</div> | |
<!-- | |
found | |
displaying | |
load more --> | |
</section> | |
</div> | |
</div> | |
<div id='map'></div> | |
<script> | |
var scope = {endpoint: undefined, collections: undefined, features: [], next_token: undefined, limit: 100, query: {}} | |
mapboxgl.accessToken = '' | |
var map = new mapboxgl.Map({ | |
container: 'map', | |
style: { | |
version: 8, | |
sources: { | |
'toner-lite': { | |
type: 'raster', | |
tiles: [ | |
'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', | |
'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', | |
'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', | |
'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png' | |
], | |
tileSize: 256, | |
attribution: | |
'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.' | |
} | |
}, | |
layers: [ | |
{ | |
'id': 'basemap', | |
'type': 'raster', | |
'source': 'toner-lite', | |
'minzoom': 0, | |
'maxzoom': 20 | |
} | |
] | |
}, | |
center: [0, 0], | |
zoom: 1 | |
}) | |
map.addControl(new mapboxgl.NavigationControl(), "top-right"); | |
var Draw = new MapboxDraw({ | |
displayControlsDefault: false, | |
}); | |
document.getElementById('bbox').addEventListener('click', () => { | |
if (Draw.getMode() !== 'simple_select') Draw.changeMode('simple_select'); | |
Draw.deleteAll(); | |
Draw.changeMode('draw_polygon') | |
}) | |
map.addControl(Draw); | |
// date picker | |
const startpicker = window.pickadate.create() | |
const startdate = document.getElementById('start-date') | |
window.pickadate.render(startdate, startpicker) | |
startdate.addEventListener('pickadate:change', () => { | |
startdate.value = startpicker.getValue('YYYY-MM-DD[T]HH:mm:ss[Z]') | |
}) | |
const endpicker = window.pickadate.create() | |
const enddate = document.getElementById('end-date') | |
window.pickadate.render(enddate, endpicker) | |
enddate.addEventListener('pickadate:change', () => { | |
enddate.value = endpicker.getValue('YYYY-MM-DD[T]HH:mm:ss[Z]') | |
}) | |
const zoomtofeature = (id) => { | |
const bbox = scope.features.filter(e => {return e.id === id})[0].bbox | |
map.fitBounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]]) | |
} | |
const query = (body) => { | |
return fetch(`${scope.endpoint}/search`, { | |
method: 'POST', | |
body: JSON.stringify(body), | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
}) | |
.then(res => { | |
if (res.ok) return res.json() | |
throw new Error('Network response was not ok.') | |
}) | |
.then((data) => { | |
data.features = data.features.map(e => { | |
e.properties.id = e.id; | |
e.properties.collection = e.collection; | |
return e; | |
}) | |
if (data.context.matched === 0) { | |
throw Error("No item found") | |
} | |
if (map.getLayer('shapes')) map.removeLayer('shapes') | |
if (map.getLayer('shapes-selected')) map.removeLayer('shapes-selected') | |
if (map.getSource('shapes')) map.removeSource('shapes') | |
map.addSource('shapes', { | |
'type': 'geojson', | |
'data': { | |
type: 'FeatureCollection', | |
features: data.features, | |
} | |
}) | |
map.addLayer({ | |
id: 'shapes', | |
type: 'fill', | |
source: 'shapes', | |
paint: { | |
'fill-color': 'rgba(200, 100, 240, 0.4)', | |
'fill-outline-color': '#FFF', | |
'fill-opacity': 1 | |
} | |
}) | |
map.addLayer({ | |
id: 'shapes-selected', | |
type: 'line', | |
source: 'shapes', | |
layout: { | |
'line-cap': 'round', | |
'line-join': 'round' | |
}, | |
paint: { | |
'line-color': '#F00', | |
'line-width': 3 | |
}, | |
filter: ['==', 'id', ''] | |
}) | |
// next_token | |
const tokens = data.links.filter(e => {return e.rel === 'next'}) | |
if (tokens.length === 1) { | |
scope.next_token = tokens[0].body.token | |
document.getElementById('btn-load-more').classList.remove('none') | |
} else { | |
document.getElementById('btn-load-more').classList.add('none') | |
} | |
const items_el = document.getElementById('results-items') | |
// Feed result table | |
for(let i = 0; i < data.features.length; i++) { | |
items_el.innerHTML += | |
`<div class="list-element" onclick="zoomtofeature('${data.features[i].id}')">` + | |
'<div class="col">' + | |
'<div class="item-descr">'+ | |
`<span class="id">${data.features[i].collection} | ${data.features[i].id}</span>` + | |
`<span class="date"><svg class='icon icon--l inline-block'><use xlink:href='#icon-clock'/></svg> ${data.features[i].properties.datetime || data.features[i].properties.start_datetime}</span>` + | |
'</div>' + | |
'</div>' + | |
'</div>' | |
} | |
return data | |
}) | |
} | |
// Add Collection filter | |
document.getElementById('add-collection').addEventListener('click', () => { | |
const collection_el = document.getElementById('collection-filters') | |
collection_el.insertAdjacentHTML( | |
'beforeend', | |
`<div><div class='select-container'>` + | |
`<select id='collection-name' class='select select--s select--border-gray wmax180 w300-ml'>` + | |
scope.collections.map(e => {return `<option value='${e}'>${e}</option>`}).join() + | |
`</select>` + | |
`<div class='select-arrow color-black'></div>` + | |
'</div>'+ | |
`<button class='btn btn--s btn--stroke color-black round ml6 opacity75-on-hover' title='Add' onclick='delete_filter(this)'><svg class='icon icon--l inline-block'><use xlink:href='#icon-trash'/></svg></button>` + | |
'</div>' | |
); | |
}) | |
// Add property filter | |
document.getElementById('add-filter').addEventListener('click', () => { | |
const collection_el = document.getElementById('property-filters') | |
collection_el.insertAdjacentHTML( | |
'beforeend', | |
`<div>` + | |
`<input id="filter-field" class='input input--s wmax60 wmax180-ml inline-block align-center color-black'/>` + | |
`<div class='select-container'>` + | |
`<select id='filter-operator' class='select select--s select--stroke color-black'>` + | |
`<option value='eq'>eq</option>` + | |
`<option value='ne'>ne</option>` + | |
`<option value='lt'>lt</option>` + | |
`<option value='le'>le</option>` + | |
`<option value='gt'>gt</option>` + | |
`<option value='ge'>ge</option>` + | |
`</select>` + | |
`<div class='select-arrow color-black'></div>` + | |
`</div>` + | |
`<input id="filter-value" class='input input--s wmax60 wmax180-ml inline-block align-center color-black'/>` + | |
`<div class='select-container'>` + | |
`<select id='filter-value-type' class='select select--s select--stroke color-black'>` + | |
`<option value='str'>str</option>` + | |
`<option value='int'>int</option>` + | |
`<option value='float'>float</option>` + | |
`</select>` + | |
`<div class='select-arrow color-black'></div>` + | |
`</div>` + | |
`<button class='btn btn--s btn--stroke color-black round ml6 opacity75-on-hover' title='Add' onclick='delete_filter(this)'><svg class='icon icon--l inline-block'><use xlink:href='#icon-trash'/></svg></button>` + | |
`</div>` | |
) | |
}) | |
const delete_filter = (e) => { | |
const parent = e.parentNode | |
parent.parentNode.removeChild(parent) | |
} | |
const renderItems = (features) => { | |
if (map.getLayer('shapes')) map.removeLayer('shapes') | |
if (map.getLayer('shapes-selected')) map.removeLayer('shapes-selected') | |
if (map.getSource('shapes')) map.removeSource('shapes') | |
map.addSource('shapes', { | |
'type': 'geojson', | |
'data': { | |
type: 'FeatureCollection', | |
features: features, | |
} | |
}) | |
map.addLayer({ | |
id: 'shapes', | |
type: 'fill', | |
source: 'shapes', | |
paint: { | |
'fill-color': 'rgba(200, 100, 240, 0.4)', | |
'fill-outline-color': '#FFF', | |
'fill-opacity': 1 | |
} | |
}) | |
map.addLayer({ | |
id: 'shapes-selected', | |
type: 'line', | |
source: 'shapes', | |
layout: { | |
'line-cap': 'round', | |
'line-join': 'round' | |
}, | |
paint: { | |
'line-color': '#F00', | |
'line-width': 3 | |
}, | |
filter: ['==', 'id', ''] | |
}) | |
} | |
document.getElementById('btn-load-more').addEventListener('click', () => { | |
body = scope.query | |
if (scope.next_token !== undefined) body.token = scope.next_token | |
query(body) | |
.then(data => { | |
scope.features = scope.features.concat(data.features) | |
minx = Math.min(...scope.features.map(e => e.bbox[0])) | |
miny = Math.min(...scope.features.map(e => e.bbox[1])) | |
maxx = Math.max(...scope.features.map(e => e.bbox[2])) | |
maxy = Math.max(...scope.features.map(e => e.bbox[3])) | |
map.fitBounds([[minx, miny], [maxx, maxy]]) | |
renderItems(scope.features) | |
}) | |
}) | |
// RESET UI | |
document.getElementById('btn-reset').addEventListener('click', () => { | |
cleanup() | |
Draw.deleteAll() | |
document.getElementById('collection-filters').innerHTML = '' | |
document.getElementById('property-filters').innerHTML = '' | |
document.getElementById('start-date').value = '' | |
document.getElementById('end-date').value = '' | |
}) | |
const cleanup = () => { | |
if (map.getLayer('shapes')) map.removeLayer('shapes') | |
if (map.getLayer('shapes-selected')) map.removeLayer('shapes-selected') | |
if (map.getSource('shapes')) map.removeSource('shapes') | |
scope.features = [] | |
scope.next_token = undefined | |
scope.query = {} | |
document.getElementById('results-items').innerHTML = '' | |
document.getElementById('btn-load-more').classList.add('none') | |
} | |
// START QUERY | |
document.getElementById('btn-query').addEventListener('click', () => { | |
cleanup() | |
// CQL2 query | |
body = {limit: scope.limit} | |
filter = {op: 'and', args: []} | |
// geom filter | |
const draws = Draw.getAll() | |
if (draws.features.length !== 0) filter.args.push({"op": "s_intersects", "args": [{"property": "geometry"}, draws.features[0].geometry]}) | |
// date filters | |
const startdate = document.getElementById('start-date').value | |
const enddate = document.getElementById('end-date').value | |
if (startdate !== '' && enddate !== '') { | |
filter.args.push( | |
{"op": "t_intersects", "args": [{"property": "datetime"}, [startdate, enddate]]} | |
) | |
} else if (startdate !== '' && enddate === '') { | |
filter.args.push( | |
{ | |
"op": "ge", | |
"args": [ { "property": "datetime" }, startdate ] | |
} | |
) | |
} else if (startdate === '' && enddate !== '') { | |
filter.args.push( | |
{ | |
"op": "le", | |
"args": [ { "property": "datetime" }, enddate ] | |
} | |
) | |
} | |
// collection filters | |
let collections = [] | |
document.getElementById('collection-filters').childNodes.forEach((e) => { | |
const collection = e.querySelector("#collection-name").value | |
collections.push(collection) | |
}) | |
if (collections.length > 0) filter.args.push({"op": "in", "args": [{"property": "collection"}, collections]}) | |
// Properties filters | |
document.getElementById('property-filters').childNodes.forEach((e) => { | |
const field = e.querySelector("#filter-field").value | |
if (field === '') return | |
const operator = e.querySelector("#filter-operator").value | |
let value = e.querySelector("#filter-value").value | |
if (value === '') return | |
const type = e.querySelector("#filter-value-type").value | |
if (type === "int"){ | |
value = parseInt(value) | |
} else if (type === "float") { | |
value = parseFloat(value) | |
} | |
filter.args.push({"op": operator, "args": [{"property": field}, value]}) | |
}) | |
if (filter.args.length > 0) body.filter = filter | |
scope.query = body | |
query(body) | |
.then(data => { | |
scope.features = data.features | |
minx = Math.min(...scope.features.map(e => e.bbox[0])) | |
miny = Math.min(...scope.features.map(e => e.bbox[1])) | |
maxx = Math.max(...scope.features.map(e => e.bbox[2])) | |
maxy = Math.max(...scope.features.map(e => e.bbox[3])) | |
map.fitBounds([[minx, miny], [maxx, maxy]]) | |
renderItems(scope.features) | |
}) | |
.catch(err => { | |
console.log(err) | |
}) | |
.then(() => { | |
document.getElementById('query').classList.toggle('active') | |
document.getElementById('results').classList.toggle('active') | |
document.getElementById(`query-section`).classList.toggle('active') | |
document.getElementById(`results-section`).classList.toggle('active') | |
}) | |
}) | |
const switchPane = (event) => { | |
const cur = document.getElementById('toolbar').querySelector(".active") | |
const activeViz = cur.id | |
const nextViz = event.id | |
cur.classList.toggle('active') | |
event.classList.toggle('active') | |
const curSection = document.getElementById(`${activeViz}-section`) | |
curSection.classList.toggle('active') | |
const nextSection = document.getElementById(`${nextViz}-section`) | |
nextSection.classList.toggle('active') | |
} | |
map.on('load', () => { | |
map.on('click', 'shapes', (e) => { | |
let html = '<table>' | |
let feat = e.features[0] | |
console.log(e.features) | |
for (const [key, value] of Object.entries(feat.properties)) { | |
html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>` | |
} | |
html += '</table>' | |
new mapboxgl.Popup() | |
.setLngLat(e.lngLat) | |
.setHTML(html) | |
.addTo(map); | |
}); | |
}) | |
document.getElementById('launch').addEventListener('click', () => { | |
scope.endpoint = document.getElementById('stacapi').value | |
document.getElementById('selector').classList.toggle('none') | |
// get collections | |
fetch(`${scope.endpoint}/collections`) | |
.then(res => { | |
if (res.ok) return res.json() | |
throw new Error('Network response was not ok.') | |
}) | |
.then((data) => { | |
console.log(data) | |
scope.collections = data.collections.map(d => {return d.id}) | |
}) | |
}) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment