diff --git a/.gitignore b/.gitignore index ebcdfe30..411c75b5 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,6 @@ lib/data/spatial/forest_types_tg lib/data/spatial/forest_types_vd lib/data/spatial/forest_types_zh lib/data/spatial/forest_types_zh_2 -lib/data/spatial/forest_types_union lib/data/spatial/silver_fir_areas lib/data/spatial/cantonal_boundaries.zip lib/data/spatial/*.zip diff --git a/components/ForestEcoregionField.tsx b/components/ForestEcoregionField.tsx index 52f8188b..fc4c7b92 100644 --- a/components/ForestEcoregionField.tsx +++ b/components/ForestEcoregionField.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { TreeAppLanguage } from "@/i18n/i18next"; import useStore from "@/store"; +import { getTypeOptions } from "@/utils/getTypeOptions"; import Dropdown, { DropdownOption } from "./ui/Dropdown"; import Input from "./ui/Input"; @@ -26,24 +27,16 @@ function ForestEcoregionField() { const options = useMemo(() => { if (projectionMode === "m") return []; - const forestEcoregions = treeClient.getTypes( - "forestecoregion", - ["code", i18n.language], - opts?.forestEcoregion?.length - ? { - code: `IN (${opts.forestEcoregion.map((ecoRegion) => `'${ecoRegion}'`).join(", ")})`, - } - : undefined, - ); - - return ( - forestEcoregions - .filter((ecoRegion) => opts.forestEcoregion?.includes(ecoRegion.code)) - .map((ecoRegion) => ({ - label: ecoRegion[i18n.language as TreeAppLanguage], - value: ecoRegion.code, - })) ?? [] - ); + return getTypeOptions({ + codes: opts?.forestEcoregion ?? [], + columns: ["code", i18n.language], + mapOption: (ecoRegion) => ({ + label: ecoRegion[i18n.language as TreeAppLanguage], + value: ecoRegion.code, + }), + treeClient, + type: "forestecoregion", + }); }, [opts?.forestEcoregion, i18n.language, treeClient, projectionMode]); if (projectionMode === "m") { diff --git a/components/LocationForm/IndicatorField.tsx b/components/LocationForm/IndicatorField.tsx index f7ddd559..f4abb73d 100644 --- a/components/LocationForm/IndicatorField.tsx +++ b/components/LocationForm/IndicatorField.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { TreeAppLanguage } from "@/i18n/i18next"; import useStore from "@/store"; +import { getTypeOptions } from "@/utils/getTypeOptions"; import LatinSwitcher from "../LatinSwitcher"; import Dropdown from "../ui/Dropdown"; @@ -20,24 +21,18 @@ function IndicatorField() { const latinActive = useStore((state) => state.latinActive); const options = useMemo(() => { - if (!opts?.indicator?.length) return []; - const indicators = treeClient.getTypes( - "indicator", - ["code", "la", i18n.language], - opts?.indicator?.length - ? { code: `IN (${opts.indicator.map((ind) => `${ind}`).join(", ")})` } - : undefined, - ); - - return ( - indicators - .filter((ind) => opts.indicator?.includes(ind.code)) - .map((ind) => ({ - filterValue: `${ind.la} ${ind[i18n.language as TreeAppLanguage]}`, - label: latinActive ? ind.la : ind[i18n.language as TreeAppLanguage], - value: ind.code, - })) ?? [] - ); + return getTypeOptions({ + codes: opts?.indicator ?? [], + columns: ["code", "la", i18n.language], + mapOption: (ind) => ({ + filterValue: `${ind.la} ${ind[i18n.language as TreeAppLanguage]}`, + label: latinActive ? ind.la : ind[i18n.language as TreeAppLanguage], + value: ind.code, + }), + quoteCodes: false, + treeClient, + type: "indicator", + }); }, [opts?.indicator, i18n.language, treeClient, latinActive]); return ( diff --git a/components/LocationForm/SilverFirAreaField.tsx b/components/LocationForm/SilverFirAreaField.tsx index 9d59f5be..75b8ea5d 100644 --- a/components/LocationForm/SilverFirAreaField.tsx +++ b/components/LocationForm/SilverFirAreaField.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { TreeAppLanguage } from "@/i18n/i18next"; import useStore from "@/store"; +import { getTypeOptions } from "@/utils/getTypeOptions"; import Dropdown from "../ui/Dropdown"; import Input from "../ui/Input"; @@ -23,24 +24,16 @@ function SilverFirAreaField() { const options = useMemo(() => { if (projectionMode === "m" || !opts?.silverFirArea?.length) return []; - const silverFirAreas = treeClient.getTypes( - "silverfirarea", - ["code", i18n.language], - opts?.silverFirArea?.length - ? { - code: `IN (${opts.silverFirArea.map((sfa) => `'${sfa}'`).join(", ")})`, - } - : undefined, - ); - - return ( - silverFirAreas - .filter((sfa) => opts.silverFirArea?.includes(sfa.code)) - .map((sfa) => ({ - label: sfa[i18n.language as TreeAppLanguage], - value: sfa.code, - })) ?? [] - ); + return getTypeOptions({ + codes: opts.silverFirArea, + columns: ["code", i18n.language], + mapOption: (sfa) => ({ + label: sfa[i18n.language as TreeAppLanguage], + value: sfa.code, + }), + treeClient, + type: "silverfirarea", + }); }, [opts?.silverFirArea, i18n.language, treeClient, projectionMode]); if (projectionMode === "m") { diff --git a/components/LocationForm/TreeTypesField.tsx b/components/LocationForm/TreeTypesField.tsx index 9d19f172..1e92aefd 100644 --- a/components/LocationForm/TreeTypesField.tsx +++ b/components/LocationForm/TreeTypesField.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { TreeAppLanguage } from "@/i18n/i18next"; import useStore from "@/store"; +import { getTypeOptions } from "@/utils/getTypeOptions"; import LatinSwitcher from "../LatinSwitcher"; import Dropdown from "../ui/Dropdown"; @@ -20,24 +21,17 @@ function TreeTypesField() { const latinActive = useStore((state) => state.latinActive); const options = useMemo(() => { - if (!opts?.treeType?.length) return []; - const treeTypes = treeClient.getTypes( - "treetype", - ["code", "la", i18n.language], - opts?.treeType?.length - ? { code: `IN (${opts.treeType.map((tt) => `'${tt}'`).join(", ")})` } - : undefined, - ); - - return ( - treeTypes - .filter((tt) => opts.treeType?.includes(tt.code)) - .map((tt) => ({ - filterValue: `${tt.la} ${tt[i18n.language as TreeAppLanguage]}`, - label: latinActive ? tt.la : tt[i18n.language as TreeAppLanguage], - value: tt.code, - })) ?? [] - ); + return getTypeOptions({ + codes: opts?.treeType ?? [], + columns: ["code", "la", i18n.language], + mapOption: (tt) => ({ + filterValue: `${tt.la} ${tt[i18n.language as TreeAppLanguage]}`, + label: latinActive ? tt.la : tt[i18n.language as TreeAppLanguage], + value: tt.code, + }), + treeClient, + type: "treetype", + }); }, [opts, i18n.language, treeClient, latinActive]); return ( diff --git a/components/LocationForm/index.tsx b/components/LocationForm/index.tsx index 6473bbda..e79ff060 100644 --- a/components/LocationForm/index.tsx +++ b/components/LocationForm/index.tsx @@ -26,6 +26,13 @@ function LocationForm() { const { t } = useTranslation(); const formLocation = useStore((state) => state.formLocation); const setFormLocation = useStore((state) => state.setFormLocation); + const hasFilters = + intersection( + Object.keys(formLocation).filter( + (key): key is keyof Location => !!formLocation[key as keyof Location], + ), + filterFields, + )?.length > 0; return (
@@ -87,13 +94,7 @@ function LocationForm() { }, ]} /> - {intersection( - Object.keys(formLocation).filter( - (key): key is keyof Location => - formLocation[key as keyof Location] !== "", - ), - filterFields, - )?.length > 0 && ( + {hasFilters && ( + ); })} diff --git a/components/LocationResult/ForestTypeButton.tsx b/components/LocationResult/ForestTypeButton.tsx new file mode 100644 index 00000000..81715d6c --- /dev/null +++ b/components/LocationResult/ForestTypeButton.tsx @@ -0,0 +1,35 @@ +import { useRouter } from "next/router"; + +import useStore from "@/store"; + +import Button, { ButtonProps } from "../ui/Button"; + +interface ForestTypeButtonProps extends ButtonProps { + code: string; +} + +export default function ForestTypeButton({ + children, + className, + code, + disabled, + ...props +}: ForestTypeButtonProps) { + const setFormLocation = useStore((state) => state.setFormLocation); + const setProjectionMode = useStore((state) => state.setProjectionMode); + const router = useRouter(); + return ( + + ); +} diff --git a/components/LocationResult/index.tsx b/components/LocationResult/index.tsx index 3cb5e2b5..6490a4ba 100644 --- a/components/LocationResult/index.tsx +++ b/components/LocationResult/index.tsx @@ -1,13 +1,12 @@ -import { useRouter } from "next/router"; import { useTranslation } from "react-i18next"; import useStore from "@/store"; -import Button from "../ui/Button"; import Message from "../ui/Message"; import InfoModal from "../ui/Modal"; import Ecogram from "./Ecogram"; +import ForestTypeButton from "./ForestTypeButton"; import type { ForestType, TreeLocationGroup } from "@geops/tree-lib/types"; @@ -22,16 +21,26 @@ const otherForestTypeGroups: TreeLocationGroup[] = [ function LocationResult() { const { i18n, t } = useTranslation(); - const router = useRouter(); + const location = useStore((state) => state.location); const formLocation = useStore((state) => state.formLocation); + const projectionMode = useStore((state) => state.projectionMode); const treeClient = useStore((state) => state.treeClient); - const setFormLocation = useStore((state) => state.setFormLocation); const { ecogram, forestTypes } = useStore((state) => state.locationResult); + console.log(formLocation); + const hasMainGroup = - !formLocation.groups || formLocation.groups.includes("main"); + !formLocation.groups || formLocation.groups?.includes("main"); const hasOtherGroup = !formLocation.groups || - formLocation.groups.filter((group) => group !== "main").length > 0; + formLocation.groups.every((group) => group !== "main"); + const hasRequiredFields = + projectionMode === "m" + ? !!(location.altitudinalZone && location.forestEcoregion) + : !!(formLocation.altitudinalZone && formLocation.forestEcoregion); + const requirementsMessage = + projectionMode === "m" + ? "projection.missingLocation" + : "location.selectAzAndEcoregion"; return forestTypes ? (
@@ -61,6 +70,9 @@ function LocationResult() { {t("location.otherResultHelp")}
+ {!hasRequiredFields && ( + {t(requirementsMessage)} + )}
{otherForestTypeGroups .filter((group) => forestTypes[group].length > 0) @@ -73,24 +85,19 @@ function LocationResult() { ftCode, ["code", i18n.language as TreeAppLanguage], ); - const onClick = () => { - setFormLocation({ forestType: ftCode }); - void router.push( - `/projection${window.location.search}`, - ); - }; return (
  • - +
  • ); })} diff --git a/components/ProjectionForm/ForestTypeField.tsx b/components/ProjectionForm/ForestTypeField.tsx index 3197dead..3f9977d7 100644 --- a/components/ProjectionForm/ForestTypeField.tsx +++ b/components/ProjectionForm/ForestTypeField.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import useStore from "@/store"; +import { getTypeOptions } from "@/utils/getTypeOptions"; import Dropdown, { DROPDOWN_CLASSNAMES, @@ -41,32 +42,24 @@ function ForestTypeField({ isTransition = false, ...props }: FtfProps) { }, [ftOpts, isTransition]); const options = useMemo(() => { - if (!forestTypeOptions?.length) return []; - const forestTypes = treeClient.getTypes( - "foresttype", - ["code", i18n.language], - forestTypeOptions?.length - ? { - code: `IN (${forestTypeOptions.map((ft) => `'${ft}'`).join(", ")})`, - } - : undefined, - ); - return ( - forestTypes - .filter((ft) => forestTypeOptions?.includes(ft.code)) - .map((ft) => ({ - label: ( -
    - {ft.code} - - - - {ft[i18n.language as TreeAppLanguage]} - -
    - ), - value: ft.code, - })) ?? [] - ); + return getTypeOptions({ + codes: forestTypeOptions ?? [], + columns: ["code", i18n.language], + mapOption: (ft) => ({ + label: ( +
    + {ft.code} + - + + {ft[i18n.language as TreeAppLanguage]} + +
    + ), + value: ft.code, + }), + treeClient, + type: "foresttype", + }); }, [forestTypeOptions, treeClient, i18n.language]); if (!forestTypeOptions?.length) return null; diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index 73480fec..009cc3b3 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -3,7 +3,7 @@ import { Button as HuiButton } from "@headlessui/react"; import type { ComponentPropsWithoutRef } from "react"; import type { ReactNode } from "react"; -interface ButtonProps extends ComponentPropsWithoutRef<"button"> { +export interface ButtonProps extends ComponentPropsWithoutRef<"button"> { children: ReactNode; className?: string; variant?: "outlined" | "primary" | "secondary"; diff --git a/i18n/resources/de/translation.json b/i18n/resources/de/translation.json index 34fa386f..f646c727 100644 --- a/i18n/resources/de/translation.json +++ b/i18n/resources/de/translation.json @@ -370,7 +370,8 @@ "mainResultHelp": "Zonaler Standortstyp, der für die entsprechende Vegetationshöhenstufe durch die Standortsfaktoren Bodensäure bzw. Nährstoffverfügbarkeit und die durchschnittliche Bodenfeuchte genügend charakterisiert und im Ökogramm dargestellt werden kann.\nBei Anwendung der Filterkriterien werden Standortstypen, welche die Kriterien erfüllen, grün hinterlegt, und Standortstypen, die sie nicht erfüllen, grau hinterlegt. ", "noEcogram": "Kein Ökogramm gefunden", "otherResultHelp": "Weitere an diesem Ort mögliche Standortstypen, die nicht zu den Hauptwaldstandorten (s. Ökogramm) gehören, sondern zu einer der folgenden 4 Standortsgruppen: Sonderwaldstandorte, Standorte mit stark wechselnde Feuchtigkeit, Auenwälder sowie Pionierstandorte und Gebüschwälder (s. Glossar).", - "reset": "Filterkriterien zurücksetzen" + "reset": "Filterkriterien zurücksetzen", + "selectAzAndEcoregion": "Bitte wählen Sie eine Standortsregion und eine Höhenstufe aus." }, "map": { "altitudinalZones": "Höhenstufen", diff --git a/i18n/resources/fr/translation.json b/i18n/resources/fr/translation.json index d90b82da..a4ecdf6a 100644 --- a/i18n/resources/fr/translation.json +++ b/i18n/resources/fr/translation.json @@ -369,7 +369,8 @@ "mainResultHelp": "Type de station zonale qui, selon l’étage de végétation correspondant, est suffisamment caractérisé par les facteurs du milieu tels que l’acidité du sol (ou les éléments nutritifs disponibles) et l’humidité moyenne du sol et qui peut être représenté dans l’écogramme. Lors de l’utilisation des critères de filtrage, les types de station qui répondent aux critères sont surlignés en vert, tandis que ceux qui ne remplissent pas les critères sont surlignés en gris.", "noEcogram": "Aucun écogramme trouvé", "otherResultHelp": "Autres types de station possibles à cet emplacement qui n’appartiennent pas aux stations forestières principales (cf. écogramme), mais aux 4 groupes de stations suivants : stations forestières particulières, stations caractérisées par une humidité très variable, forêts alluviales ainsi que stations pionnières et forêts buissonnantes (cf. glossaire).", - "reset": "Enlever les critères de filtrage" + "reset": "Enlever les critères de filtrage", + "selectAzAndEcoregion": "Veuillez sélectionner un étage de végétation et une écorégion" }, "map": { "altitudinalZones": "Étages de végétation", diff --git a/lib/README.md b/lib/README.md index 143becef..e2c2c5a4 100644 --- a/lib/README.md +++ b/lib/README.md @@ -4,7 +4,7 @@ This library provides tree recommendations for different climate change scenario ## Data -Data for tree type projections is provided as a [CSV file](./data/projections.csv) and needs to be converted into JSON to be usable by the library. +Data for tree type projections is provided as a [CSV file](./data/projections.csv) and needs to be converted into an sqlite database to be usable by the library. 1. Install NodeJS, pnpm, vercel CLI and Docker Compose. 2. Install dependencies: `pnpm install` @@ -26,7 +26,7 @@ Spatial data is downloaded from different sources and imported into the database 5. Generate font glyphs for Mapbox GL: `pnpm run data:spatial:fonts` (you might have to use an earlier node version for this) 6. Deploy tiles local by running `pnpm run data:spatial:deploy:local` and change REACT_APP_VECTOR_TILES_ENDPOINT to localhost in `.env` (reload new endppoint with `pnpm dev`) 7. Change version number in [src/service-worker.js](https://github.com/geops/tree-app/blob/master/src/service-worker.js#L16) to clear the tile cache and deploy repository changes -8. Before deployment, check the vercel project ID in the `.vercel` folder in [tiles](https://github.com/geops/tree-app/tree/master/lib/data/spatial/tiles). This folder is generated during the first deployemnt and prompts the definition of the vercel project. Depending on the current project ID and the target instance (`tree-app` or `tg.tree-app`), the folder needs to be deleted so the initial prompt is triggered again (e.g. if Thurgau tiles `tiles-tg` need to be deployed but the last deployment was made for '`tiles-staging`', the folder needs to be recreated) +8. Before deployment, check the vercel project ID in the `.vercel` folder in [tiles](https://github.com/geops/tree-app/tree/master/lib/data/spatial/tree-app-tiles). This folder is generated during the first deployemnt and prompts the definition of the vercel project. Depending on the current project ID and the target instance (`tree-app` or `tg.tree-app`), the folder needs to be deleted so the initial prompt is triggered again (e.g. if Thurgau tiles `tiles-tg` need to be deployed but the last deployment was made for '`tiles-staging`', the folder needs to be recreated) 9. Deploy tiles to a webserver or to Vercel (currently maintained by geOps) running `pnpm run data:spatial:deploy` followed by `vercel alias set [deployment-url] [custom-domain]` (custom domain either `tiles.tree-app.ch`, `tiles-staging.tree-app.ch`, `tiles-tg.tree-app.ch`). The geops vercel team needs to be active locally (use https://vercel.com/docs/cli/switch) for this step. ## Excluding specific canton data diff --git a/lib/data/spatial/1-import.sh b/lib/data/spatial/1-import.sh index 6d908573..681aafdb 100755 --- a/lib/data/spatial/1-import.sh +++ b/lib/data/spatial/1-import.sh @@ -3,6 +3,8 @@ _import () { local TARGET=$2 # cantonal_boundaries local ZIPFILE=$3 # SHAPEFILE_LV95_LN02 local SHPFILE=$4 # swissBOUNDARIES3D_1_3_TLM_KANTONSGEBIET + local SHP_PATH="/data/spatial/${TARGET}/${TARGET}.shp" + local GPKG_PATH="/data/spatial/${TARGET}/${ZIPFILE}.gpkg" # if [ "$URL" == "Download manually" ]; then # if [ ! -f "/data/spatial/${TARGET}.zip" ]; then @@ -21,17 +23,28 @@ _import () { # fi # fi - if [ "$URL" == "Download manually" ] && [ ! -f "/data/spatial/${TARGET}/${TARGET}.shp" ]; then + if [ "$URL" == "Download manually" ]; then + if [ ! -f "$SHP_PATH" ] && [ -d "/data/spatial/${TARGET}" ]; then + SHP_PATH=$(find "/data/spatial/${TARGET}" -maxdepth 1 -type f -name "*.shp" | head -n 1) + fi + + if [ ! -f "$GPKG_PATH" ] && [ -d "/data/spatial/${TARGET}" ]; then + GPKG_PATH=$(find "/data/spatial/${TARGET}" -maxdepth 1 -type f -name "*.gpkg" | head -n 1) + fi + fi + + if [ "$URL" == "Download manually" ] && [ ! -f "$SHP_PATH" ] && [ ! -f "$GPKG_PATH" ]; then echo "Manual unzipped download for ${TARGET} is missing! Do nothing ..." return fi - if [ ! -f "/data/spatial/${TARGET}/${TARGET}.shp" ] && [ ! -f "/data/spatial/${TARGET}/${ZIPFILE}.gpkg" ]; then + if [ ! -f "$SHP_PATH" ] && [ ! -f "$GPKG_PATH" ]; then echo "Downloading ${TARGET} ..." mkdir -p /data/spatial/${TARGET} if [[ "$URL" == *.gpkg ]]; then wget --no-check-certificate "${URL}" -O "/data/spatial/${TARGET}/${ZIPFILE}.gpkg" + GPKG_PATH="/data/spatial/${TARGET}/${ZIPFILE}.gpkg" else cd /data/spatial wget --no-check-certificate "${URL}" -O "${TARGET}.zip" @@ -47,6 +60,7 @@ _import () { fi fi rename "s/${SHPFILE}/${TARGET}/" ${SHPFILE}.* + SHP_PATH="/data/spatial/${TARGET}/${TARGET}.shp" fi else echo "$TARGET already downloaded! Will be reused ..." @@ -54,12 +68,12 @@ _import () { local COUNT=$(psql -d tree -U postgres -At -c "SELECT COUNT(*) FROM ${TARGET}") if [ "$COUNT" == "0" ]; then - if [[ -n "$ZIPFILE" ]] && [ -f "/data/spatial/${TARGET}/${ZIPFILE}.gpkg" ]; then - echo "Importing GeoPackage ${ZIPFILE}.gpkg ..." - ogr2ogr -f PostgreSQL "PG:dbname=tree user=postgres" -nln ${TARGET} /data/spatial/${TARGET}/${ZIPFILE}.gpkg + if [ -f "$GPKG_PATH" ]; then + echo "Importing GeoPackage $(basename "$GPKG_PATH") ..." + ogr2ogr -f PostgreSQL "PG:dbname=tree user=postgres" -nln ${TARGET} "$GPKG_PATH" else - echo "Importing Shapefile ${TARGET}.shp ..." - shp2pgsql -D -a -s 2056 "/data/spatial/${TARGET}/${TARGET}.shp" | psql -d tree -U postgres + echo "Importing Shapefile $(basename "$SHP_PATH") ..." + shp2pgsql -D -a -s 2056 "$SHP_PATH" | psql -d tree -U postgres fi else echo "$TARGET already imported! Do nothing ..." @@ -86,7 +100,7 @@ _import "Download manually" "forest_types_lu" "forest_types_lu" "forest_types_lu _import "Download manually" "forest_types_ne" "forest_types_ne" "forest_types_ne" -# _import "Download manually" "forest_types_tg" "forest_types_tg" "forest_types_tg" +_import "Download manually" "forest_types_tg" "forest_types_tg" "forest_types_tg" _import "Download manually" "forest_types_zh" "forest_types_zh" "forest_types_zh" diff --git a/lib/data/spatial/spatial-data-check.sh b/lib/data/spatial/spatial-data-check.sh index 397c6bc2..c727ce68 100644 --- a/lib/data/spatial/spatial-data-check.sh +++ b/lib/data/spatial/spatial-data-check.sh @@ -16,11 +16,10 @@ FOLDERS=( "$SCRIPT_DIR/forest_types_sh" "$SCRIPT_DIR/forest_types_so" "$SCRIPT_DIR/forest_types_sz" - "$SCRIPT_DIR/forest_types_union" "$SCRIPT_DIR/forest_types_vd" "$SCRIPT_DIR/forest_types_zh_2" "$SCRIPT_DIR/forest_types_zh" - # "$SCRIPT_DIR/forest_types_tg" + "$SCRIPT_DIR/forest_types_tg" "$SCRIPT_DIR/altitudinal_zones_1995" "$SCRIPT_DIR/altitudinal_zones_2085_dry" "$SCRIPT_DIR/altitudinal_zones_2085_less_dry" diff --git a/lib/data/spatial/tree-app-tiles/local.ts b/lib/data/spatial/tree-app-tiles/local.ts new file mode 100644 index 00000000..cdc753bf --- /dev/null +++ b/lib/data/spatial/tree-app-tiles/local.ts @@ -0,0 +1,9 @@ +import { serve } from '@hono/node-server' +import { app } from './server.js' + +serve({ + fetch: app.fetch, + port: parseInt(process.env.PORT as string, 10) || 8000, +}) + +console.log('Server running at http://localhost:8000') diff --git a/lib/data/spatial/tree-app-tiles/package.json b/lib/data/spatial/tree-app-tiles/package.json index 67668507..9887b118 100644 --- a/lib/data/spatial/tree-app-tiles/package.json +++ b/lib/data/spatial/tree-app-tiles/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "tsx watch server.ts", - "start": "node dist/server.js", + "dev": "tsx watch local.ts", + "start": "node dist/local.js", "build": "tsc" }, "dependencies": { diff --git a/lib/data/spatial/tree-app-tiles/server.ts b/lib/data/spatial/tree-app-tiles/server.ts index c55a4a4b..aecbb177 100644 --- a/lib/data/spatial/tree-app-tiles/server.ts +++ b/lib/data/spatial/tree-app-tiles/server.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono' import fs from 'fs' import path from 'path' -import { serve } from '@hono/node-server' +import { handle } from '@hono/node-server/vercel' const app = new Hono() const baseDir = path.join(process.cwd(), 'tiles') @@ -68,10 +68,5 @@ app.get('/fonts/*', async (c) => { } }) -// Start local server -serve({ - fetch: app.fetch, - port: parseInt(process.env.PORT as string, 10) || 8000, -}) - -console.log('Server running at http://localhost:8000') +export { app } +export default handle(app) diff --git a/lib/data/spatial/tree-app-tiles/tsconfig.json b/lib/data/spatial/tree-app-tiles/tsconfig.json index b3a742da..dd1bc931 100644 --- a/lib/data/spatial/tree-app-tiles/tsconfig.json +++ b/lib/data/spatial/tree-app-tiles/tsconfig.json @@ -11,5 +11,5 @@ "types": ["node"], "lib": ["ES2022", "DOM"] }, - "include": ["server.ts"] + "include": ["server.ts", "local.ts"] } \ No newline at end of file diff --git a/lib/data/spatial/tree-app-tiles/vercel.json b/lib/data/spatial/tree-app-tiles/vercel.json new file mode 100644 index 00000000..63632695 --- /dev/null +++ b/lib/data/spatial/tree-app-tiles/vercel.json @@ -0,0 +1,20 @@ +{ + "version": 2, + "builds": [ + { + "src": "server.ts", + "use": "@vercel/node", + "config": { + "includeFiles": [ + "tiles/**" + ] + } + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "/server.ts" + } + ] +} diff --git a/lib/data/sql/export_recommendations.sql b/lib/data/sql/export_recommendations.sql index f9feac6a..f0c9654d 100644 --- a/lib/data/sql/export_recommendations.sql +++ b/lib/data/sql/export_recommendations.sql @@ -75,4 +75,4 @@ begin end; $$ language plpgsql; -select create_recommendations('nat_naistyp_art', 'nat_baum_collin', 'export.recommendations'); \ No newline at end of file +select create_recommendations('nat_naistyp_art', 'nat_baum_collin', 'export.recommendations'); diff --git a/lib/data/sql/export_spatial.sql b/lib/data/sql/export_spatial.sql index 762b2751..3d5980dc 100644 --- a/lib/data/sql/export_spatial.sql +++ b/lib/data/sql/export_spatial.sql @@ -134,6 +134,17 @@ WITH altitudinal_zones_cantonal AS WHERE hs_code IS NOT NULL and geom is not null GROUP BY hs_code, hsue_code) UNION + (SELECT + ST_Union(geom) AS geom, + CASE hsue_code is null + WHEN TRUE THEN hs_code::text + ELSE hs_code::text || '(' || hsue_code::text || ')' + END AS code, + hs_code::text as code_style + FROM forest_types_tg + WHERE hs_code IS NOT NULL and geom is not null + GROUP BY hs_code, hsue_code) + UNION (SELECT ST_Union(geom) AS geom, CASE hsue_code is null @@ -461,7 +472,7 @@ delete from forest_types_ar_gen where st_area(geom) < 2000; CREATE OR REPLACE VIEW forest_types_union_export AS SELECT ST_Transform(ST_Union(geom), 3857) AS geom FROM ( - SELECT (ST_Dump(ST_MakeValid(geom))).geom AS geom FROM forest_types_tg_gen + SELECT (ST_Dump(ST_MakeValid(geom))).geom FROM forest_types_tg_gen UNION ALL SELECT (ST_Dump(ST_MakeValid(geom))).geom FROM forest_types_fl_gen UNION ALL diff --git a/lib/data/sql/import_data.sql b/lib/data/sql/import_data.sql index 921ce0c7..111e97d8 100644 --- a/lib/data/sql/import_data.sql +++ b/lib/data/sql/import_data.sql @@ -971,6 +971,40 @@ UPDATE vd_projections_import SET additional = 'flachgründig' WHERE lower(trim(additional)) = 'sol superficiel'; +CREATE TEMP TABLE VD_NAT_BAUM_COLLIN_STAGING ( + REGION TEXT, + NAISTYP_SORT TEXT, + NAISTYP TEXT, + SISF_NR TEXT, + VORH TEXT, + QUELLE_BA TEXT, + ART TEXT, + "update" TEXT +); +COPY VD_NAT_BAUM_COLLIN_STAGING +FROM '/data/profiles/vd/VD_NAT_BAUM_COLLIN.csv' DELIMITER ';' +CSV HEADER; + +CREATE TABLE VD_NAT_BAUM_COLLIN AS +SELECT REGION, NAISTYP_SORT, NAISTYP, SISF_NR, VORH, QUELLE_BA +FROM VD_NAT_BAUM_COLLIN_STAGING +WHERE update IS DISTINCT FROM 'delete'; + +CREATE TEMP TABLE VD_NAT_NAISTYP_ART_STAGING ( + NAISTYP_SORT TEXT, NAISTYP_C TEXT, + ART TEXT, SISF_NR TEXT, VORH TEXT, + "update" TEXT +); +COPY VD_NAT_NAISTYP_ART_STAGING +FROM + '/data/profiles/vd/VD_NAT_NAISTYP_ART.csv' DELIMITER ';' CSV HEADER; + +CREATE TABLE VD_NAT_NAISTYP_ART AS +SELECT NAISTYP_SORT, NAISTYP_C, + ART, SISF_NR, VORH +FROM VD_NAT_NAISTYP_ART_STAGING +WHERE update IS DISTINCT FROM 'delete'; + -- ########### PROJECTIONS ########### CREATE TABLE projections_import ( forest_ecoregions TEXT, altitudinal_zone TEXT, @@ -1135,8 +1169,27 @@ SELECT CREATE TABLE "forest_types_tg" ( gid serial, "fid" numeric, + "OBJECTID_1" numeric, + "objectid" numeric, + "reviernr" numeric, + "standortei" numeric, + "befahrbark" numeric, + "text" varchar(254), + "laubholzan" numeric, + "g1" numeric, + "g2" numeric, + "typ" numeric, + "ndhant" numeric, + "ndh_2050" numeric, + "zusatz" varchar(254), + "wuechsigk" numeric, + "Shape_Area" numeric, "tgneu" varchar(50), - "nais" varchar(50) + "nais" varchar(50), + "Hoehenstufe" varchar(50), + "tahsue" varchar(50), + "hs_code" int4, + "hsue_code" int4 ); ALTER TABLE "forest_types_tg" diff --git a/lib/package.json b/lib/package.json index 79ef33d3..b9038f89 100644 --- a/lib/package.json +++ b/lib/package.json @@ -20,7 +20,7 @@ "data:spatial:tile": "cd data && docker compose exec tippecanoe sh -c '/data/spatial/3-tile.sh'", "data:spatial:fonts": "bash ./data/spatial/4-fonts.sh", "data:spatial:deploy": "cd data/spatial/tree-app-tiles && pnpm install && pnpm build && vercel deploy --cwd .", - "data:spatial:deploy:local": "cd data/spatial/tree-app-tiles && pnpm install && pnpm build && node dist/server.js", + "data:spatial:deploy:local": "cd data/spatial/tree-app-tiles && pnpm install && pnpm build && node dist/local.js", "data:export:csv": "node --experimental-json-modules export/src/export_csv.mjs", "doc:build": "documentation build src/** --format html --output doc --config documentation.yml", "doc:serve": "documentation serve --config documentation.yml --watch src/**", diff --git a/pages/index.tsx b/pages/index.tsx index f276fd16..06844e53 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,5 +1,18 @@ -import Location from "./location"; +import type { GetServerSideProps } from "next"; + +export const getServerSideProps: GetServerSideProps = async (context) => { + const query = context.resolvedUrl.includes("?") + ? context.resolvedUrl.slice(context.resolvedUrl.indexOf("?")) + : ""; + + return { + redirect: { + destination: `/projection${query}`, + permanent: false, + }, + }; +}; export default function Index() { - return ; + return null; } diff --git a/utils/getTypeOptions.ts b/utils/getTypeOptions.ts new file mode 100644 index 00000000..312d90f9 --- /dev/null +++ b/utils/getTypeOptions.ts @@ -0,0 +1,54 @@ +interface TypeRecordWithCode { + code: string; +} + +interface TreeClientLike { + getTypes: ( + type: string, + columns?: string[], + where?: { code: string }, + ) => T[]; +} + +interface GetTypeOptionsParams { + codes: string[]; + columns: string[]; + mapOption: (record: T) => O; + quoteCodes?: boolean; + treeClient: TreeClientLike; + type: string; +} + +export const getCodeInFilter = (codes?: string[], quoteCodes = true) => { + if (!codes?.length) { + return undefined; + } + + const values = codes.map((code) => (quoteCodes ? `'${code}'` : `${code}`)); + return { code: `IN (${values.join(", ")})` }; +}; + +export const getTypeOptions = ({ + codes, + columns, + mapOption, + quoteCodes = true, + treeClient, + type, +}: GetTypeOptionsParams): O[] => { + if (!codes.length) { + return []; + } + + const codeSet = new Set(codes); + const rows = treeClient.getTypes( + type, + columns, + getCodeInFilter(codes, quoteCodes), + ); + + return ( + rows?.filter((row) => codeSet.has(row.code)).map((row) => mapOption(row)) ?? + [] + ); +};