-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor and enhance STAC data handling and user interface
- Improved STAC data structure and properties for better clarity and usability. - Enhanced frontend functionality with Tailwind CSS integration for improved styling. - Streamlined data fetching in index.vue, optimizing dataset selection and polygon drawing features. - Updated package dependencies to include @nuxtjs/tailwindcss and lucide-vue-next. - Improved user experience with clearer indicators for dataset availability and enhanced download options.
- Loading branch information
1 parent
b5537b7
commit c14f33c
Showing
27 changed files
with
911 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
<script lang="ts" setup> | ||
import { Eye, EyeOff } from 'lucide-vue-next' | ||
import { CollectionType, LayerLink } from '../types' | ||
import type { Ref } from 'vue' | ||
let { collection, onChangeActive, activeValue } = defineProps<{ | ||
collection: CollectionType & { href: string } | ||
activeValue?: LayerLink | ||
onChangeActive(link?: LayerLink): void | ||
}>() | ||
let baseURL = collection.href.replace('/collection.json', '') | ||
let summaries = computed(() => { | ||
return collection?.summaries | ||
}) | ||
let propertyOrder = computed(() => Object.keys(summaries.value)) | ||
let variables: Ref<Record<string, string | undefined>> = ref({}) | ||
function resetVariables() { | ||
variables.value = Object.keys(summaries.value ?? {}).reduce((acc, key) => { | ||
return { | ||
...acc, | ||
[key]: undefined, | ||
} | ||
}, {} as Record<string, string | undefined>) | ||
} | ||
resetVariables() | ||
function getValidOptionsForProperty(property: string) { | ||
if (!variables.value) return [] | ||
let currentIndex = propertyOrder.value.indexOf(property) | ||
let relevantProperties = propertyOrder.value.slice(0, currentIndex) | ||
let validItems = collection.links | ||
.filter((link) => link.rel === 'item') | ||
.filter((link) => { | ||
if (!variables.value) return false | ||
return Object.entries(variables.value) | ||
.filter(([key]) => relevantProperties.includes(key)) | ||
.every( | ||
([key, option]) => | ||
!option || | ||
link.properties?.[key as keyof typeof link.properties] === option, | ||
) | ||
}) | ||
return [ | ||
...new Set( | ||
validItems.map( | ||
(item) => item?.properties?.[property as keyof typeof item.properties], | ||
), | ||
), | ||
] | ||
} | ||
function selectOption(property: string, option: string) { | ||
if (!variables.value) return | ||
if (variables.value[property] === option) { | ||
variables.value[property] = undefined | ||
} else { | ||
variables.value[property] = option | ||
} | ||
let currentIndex = propertyOrder.value.indexOf(property) | ||
for (let i = currentIndex + 1; i < propertyOrder.value.length; i++) { | ||
let nextProperty = propertyOrder.value[i] | ||
if (!variables.value) break | ||
variables.value[nextProperty] = undefined | ||
let validOptions = getValidOptionsForProperty(nextProperty) | ||
if (validOptions.length === 1) { | ||
variables.value[nextProperty] = validOptions[0] | ||
} | ||
} | ||
} | ||
watchEffect(() => { | ||
if (!summaries.value || !variables.value) return | ||
let foundLink = collection.links | ||
.filter((l) => l.rel === 'item') | ||
.find((link) => { | ||
return Object.entries(variables.value ?? {}).every( | ||
([key, option]) => | ||
link.properties?.[key as keyof typeof link.properties] === option, | ||
) | ||
}) | ||
if (foundLink) { | ||
onChangeActive({ | ||
type: 'item', | ||
href: baseURL + '/' + foundLink.href, | ||
}) | ||
} else { | ||
onChangeActive(undefined) | ||
} | ||
}) | ||
function toggleActive(value: boolean) { | ||
if (!value) { | ||
onChangeActive(undefined) | ||
resetVariables() | ||
} else { | ||
if (summaries.value) { | ||
// do nothing | ||
} else { | ||
const geoserverLink = collection.assets?.['geoserver_link'] as | ||
| { href: string } | ||
| undefined | ||
if (geoserverLink) { | ||
onChangeActive({ | ||
type: 'raster', | ||
href: geoserverLink.href, | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<div v-if="!summaries"> | ||
<div class="flex items-center gap-3 bg-white p-3 shadow"> | ||
<button | ||
@click="toggleActive(!activeValue)" | ||
class="size-8 flex items-center justify-center shrink-0 rounded-md hover:bg-gray-100" | ||
> | ||
<Eye class="size-4" v-if="!!activeValue" /> | ||
<EyeOff class="size-4" v-if="!activeValue" /> | ||
</button> | ||
<div class="text-sm font-medium">{{ collection.title }}</div> | ||
</div> | ||
</div> | ||
<v-expansion-panel v-if="summaries"> | ||
<v-expansion-panel-title> | ||
<div class="flex items-center gap-3"> | ||
<button | ||
@click.stop="!!activeValue && toggleActive(false)" | ||
class="z-10 size-8 flex items-center justify-center shrink-0 rounded-md hover:bg-gray-100" | ||
> | ||
<Eye class="size-4" v-if="!!activeValue" /> | ||
<EyeOff class="size-4" v-if="!activeValue" /> | ||
</button> | ||
<div>{{ collection.title }}</div> | ||
</div> | ||
</v-expansion-panel-title> | ||
<v-expansion-panel-text> | ||
<div class="grid grid-flow-row gap-1.5 py-3"> | ||
<div | ||
v-for="(options, key) in summaries" | ||
:key="key" | ||
class="empty:hidden" | ||
> | ||
<v-select | ||
:label="key" | ||
:items="getValidOptionsForProperty(key)" | ||
:model-value="variables?.[key]" | ||
@update:model-value="selectOption(key, $event)" | ||
/> | ||
</div> | ||
</div> | ||
</v-expansion-panel-text> | ||
</v-expansion-panel> | ||
</template> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<script setup lang="ts"> | ||
import { ItemType } from '~/types' | ||
let { href } = defineProps<{ | ||
href: string | ||
}>() | ||
let { data: jsonString } = await useFetch<ItemType>(href) | ||
let item = computed(() => JSON.parse(jsonString.value)) | ||
</script> | ||
|
||
<template> | ||
<RasterLayer :id="item.id" :href="item.assets.visual.href" /> | ||
</template> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<script setup lang="ts"> | ||
import { LayerLink } from '~/types' | ||
let { link } = defineProps<{ | ||
link: LayerLink | ||
}>() | ||
</script> | ||
|
||
<template> | ||
<ItemLayer v-if="link.type === 'item'" :href="link.href" /> | ||
<RasterLayer | ||
v-if="link.type === 'raster'" | ||
:href="link.href" | ||
:id="link.href" | ||
/> | ||
</template> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<script setup lang="ts"> | ||
import { MapboxLayer } from '@studiometa/vue-mapbox-gl' | ||
let { id, href } = defineProps<{ | ||
id: string | ||
href: string | ||
}>() | ||
let layer = computed(() => { | ||
return { | ||
id, | ||
type: 'raster', | ||
source: { | ||
type: 'raster', | ||
tiles: [href], | ||
tileSize: 256, | ||
}, | ||
} | ||
}) | ||
</script> | ||
|
||
<template> | ||
<MapboxLayer v-if="layer" :key="layer.id" :id="layer.id" :options="layer" /> | ||
</template> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Copyright 2020 Google, LLC. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
# Use the official lightweight Python image. | ||
# https://hub.docker.com/_/python | ||
FROM python:3.11-slim | ||
|
||
# Allow statements and log messages to immediately appear in the Knative logs | ||
ENV PYTHONUNBUFFERED True | ||
|
||
# Copy local code to the container image. | ||
ENV APP_HOME /app | ||
WORKDIR $APP_HOME | ||
COPY . ./ | ||
|
||
# Install production dependencies. | ||
RUN pip install -r requirements.txt | ||
|
||
# Run the web service on container startup. Here we use the gunicorn | ||
# webserver, with one worker process and 8 threads. | ||
# For environments with multiple CPU cores, increase the number of workers | ||
# to be equal to the cores available. | ||
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Developing the report-python-cloud-run Function | ||
|
||
## Prerequisites | ||
|
||
Create a virtual environment and install the dependencies: | ||
|
||
```bash | ||
pip install -r requirements.txt | ||
``` | ||
|
||
## Testing | ||
|
||
Run the report function locally: | ||
|
||
```bash | ||
python report.py | ||
``` | ||
|
||
## Deploying | ||
|
||
Deploying to Cloud run is done using github actions. The workflow is defined in `.github/workflows/deploy_function.yml`. The workflow is triggered on push to the `main` branch. |
Empty file.
Binary file not shown.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import json | ||
import os | ||
|
||
from shapely import Polygon # type: ignore | ||
from shapely.geometry import shape # type: ignore | ||
from flask import Flask, make_response, render_template_string, request | ||
|
||
from report.report import ( | ||
create_report_html, | ||
create_report_pdf, | ||
POLYGON_DEFAULT, | ||
STAC_ROOT_DEFAULT, | ||
) | ||
|
||
app = Flask(__name__) | ||
|
||
|
||
@app.route("/", methods=["GET"]) | ||
def return_report(): | ||
"""Return a report for the given polygon""" | ||
polygon_str = request.args.get("polygon") | ||
|
||
if not polygon_str: | ||
polygon_str = POLYGON_DEFAULT | ||
|
||
origin = request.headers.get("Referer") | ||
print(f"detected origin: {origin}") | ||
|
||
# For now we pin the stac_root on a default because we | ||
# don't have a way to pass it in from the client and cant handle the password | ||
# protected preview deployments | ||
stac_root = STAC_ROOT_DEFAULT | ||
|
||
polygon = shape(json.loads(polygon_str)) | ||
if not isinstance(polygon, Polygon): | ||
raise ValueError("Invalid polygon") | ||
|
||
web_page_content = create_report_html(polygon=polygon, stac_root=stac_root) | ||
pdf_object = create_report_pdf(web_page_content) | ||
|
||
response = make_response(pdf_object.getvalue()) | ||
response.headers["Content-Type"] = "application/pdf" | ||
response.headers["Content-Disposition"] = "inline; filename=coastal_report.pdf" | ||
response.headers["Access-Control-Allow-Origin"] = "*" # CORS | ||
return response | ||
|
||
|
||
@app.route("/html") | ||
def return_html(): | ||
"""Return a report for the given polygon""" | ||
polygon_str = request.args.get("polygon") | ||
|
||
if not polygon_str: | ||
polygon_str = POLYGON_DEFAULT | ||
|
||
origin = request.headers.get("Referer") | ||
print(f"detected origin: {origin}") | ||
|
||
# For now we pin the stac_root on a default because we | ||
# don't have a way to pass it in from the client and cant handle the password | ||
# protected preview deployments | ||
stac_root = STAC_ROOT_DEFAULT | ||
|
||
polygon = shape(json.loads(polygon_str)) | ||
if not isinstance(polygon, Polygon): | ||
raise ValueError("Invalid polygon") | ||
|
||
web_page_content = create_report_html(polygon=polygon, stac_root=stac_root) | ||
|
||
response = make_response(render_template_string(web_page_content)) | ||
response.headers["Access-Control-Allow-Origin"] = "*" # CORS | ||
return response | ||
|
||
|
||
if __name__ == "__main__": | ||
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) |
Empty file.
Empty file.
13 changes: 13 additions & 0 deletions
13
app/functions/report-python-cloud-run/report/datasets/base_dataset.py
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from typing import Optional | ||
import xarray as xr | ||
|
||
from .datasetcontent import DatasetContent | ||
from .esl import get_esl_content | ||
|
||
|
||
def get_dataset_content(dataset_id: str, xarr: xr.Dataset) -> Optional[DatasetContent]: | ||
match dataset_id: | ||
case "esl_gwl": | ||
return get_esl_content(xarr) | ||
case _: | ||
return None |
11 changes: 11 additions & 0 deletions
11
app/functions/report-python-cloud-run/report/datasets/datasetcontent.py
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from dataclasses import dataclass | ||
from typing import Optional | ||
|
||
|
||
@dataclass | ||
class DatasetContent: | ||
dataset_id: str | ||
title: str | ||
text: str | ||
image_base64: Optional[str] = None | ||
image_svg: Optional[str] = None |
Oops, something went wrong.