diff --git a/docker-compose.previewers.yml b/docker-compose.previewers.yml index 9b2e0ee01..f77363896 100644 --- a/docker-compose.previewers.yml +++ b/docker-compose.previewers.yml @@ -35,6 +35,20 @@ services: - geoserver_data:/opt/geoserver_data - geoserver_exts:/opt/additional_libs + geoshp-preview: + image: maxzilla2/geoshp + environment: + GEOSERVER_URL: http://geoserver:8080/geoserver/ + EXTERNAL_GEOSERVER_URL: http://localhost:8085/geoserver/ + GEOSERVER_USERNAME: admin + GEOSERVER_PASSWORD: geoserver + CLOWDER_VERSION: 2 + CLOWDER_URL: http://host.docker.internal:8000/ + RABBITMQ_URI: amqp://guest:guest@host.docker.internal:5672/%2F + networks: + - clowder2 + restart: unless-stopped + ## This image must be built from: ## https://github.com/clowder-framework/extractors-geo/blob/master/preview.geotiff/Dockerfile ## docker build -f Dockerfile -t clowder/extractors-geotiff-preview . diff --git a/frontend/src/components/visualizations/Geospatial/manifest.json b/frontend/src/components/visualizations/Geospatial/manifest.json index f8a0e8114..32669230c 100644 --- a/frontend/src/components/visualizations/Geospatial/manifest.json +++ b/frontend/src/components/visualizations/Geospatial/manifest.json @@ -13,10 +13,6 @@ "name": "Geospatial", "mainType": "image", "mimeTypes": [ - "application/zip", - "application/x-zip", - "application/x-7z-compressed", - "multi/files-zipped", "image/tif", "image/tiff" ], diff --git a/frontend/src/components/visualizations/GeospatialVector/GeospatialVector.tsx b/frontend/src/components/visualizations/GeospatialVector/GeospatialVector.tsx new file mode 100644 index 000000000..60f9d3f81 --- /dev/null +++ b/frontend/src/components/visualizations/GeospatialVector/GeospatialVector.tsx @@ -0,0 +1,320 @@ +import React, { useEffect, useRef, useState } from "react"; + +import Map from "ol/Map"; +import View from "ol/View"; +import { VisualizationConfigOut } from "../../../openapi/v2"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import GeoJSON from "ol/format/GeoJSON"; +import TileLayer from "ol/layer/Tile"; +import { OSM } from "ol/source"; +import { bbox as bboxStrategy } from "ol/loadingstrategy"; +import { transformExtent } from "ol/proj"; +import { FeatureLike } from "ol/Feature"; + +type GeospatialProps = { + visConfigEntry?: VisualizationConfigOut; +}; + +export default function GeospatialVector(props: GeospatialProps) { + const { visConfigEntry } = props; + + const [layerWMS, setLayerWMS] = useState(undefined); + const [layerDL, setLayerDL] = useState(undefined); + + const [layerAttributes, setLayerAttributes] = useState( + undefined + ); + const [filterAttribute, setFilterAttribute] = useState( + undefined + ); + const [attributeValues, setAttributeValues] = useState( + undefined + ); + const [attributeValue, setAttributeValue] = useState( + undefined + ); + const [vectorRef, setVectorRef] = useState | undefined>( + undefined + ); + const [map, setMap] = useState(undefined); + const mapElement = useRef(); + + function updateFilterAttribute(event) { + setFilterAttribute(event.target.value); + } + + function setAttributeValueFn(event) { + setAttributeValue(event.target.value); + } + + function clearFilter() { + setFilterAttribute(undefined); + setAttributeValue("Show All"); + } + + useEffect(() => { + if (visConfigEntry !== undefined) { + if ( + visConfigEntry.parameters && + visConfigEntry.parameters["WMS Layer URL"] + ) { + const wms_url = String(visConfigEntry.parameters["WMS Layer URL"]); + setLayerWMS(wms_url); + + const attribute_url = wms_url.replace( + "GetFeature", + "describeFeatureType" + ); + fetch(attribute_url).then((response) => { + if (response.status === 200) { + response.json().then((json) => { + const attrs: string[] = []; + json["featureTypes"][0]["properties"].forEach((a) => { + attrs.push(a["name"]); + }); + setLayerAttributes(attrs); + }); + } + }); + } + } + }, [visConfigEntry]); + + useEffect(() => { + if (layerWMS !== undefined) { + // Determine bounding box extent & center point from URL + let bbox = [0, 0, 0, 0]; + const entries = layerWMS.split("&"); + entries.forEach((entry) => { + if (entry.startsWith("bbox=")) { + const vals = entry.replace("bbox=", "").split(","); + bbox = vals.map((v) => parseFloat(v)); + } + }); + bbox = transformExtent(bbox, "EPSG:4326", "EPSG:3857"); + const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; + + let wms_url = layerWMS; + if (attributeValue != undefined) { + const params = new URLSearchParams(wms_url.split("?")[1]); + params.delete("bbox"); + wms_url = `${wms_url.split("?")[0]}?${params.toString()}`; + wms_url += `&CQL_FILTER=${filterAttribute}='${attributeValue}'`; + } + + const source = new VectorSource({ + url: wms_url, + format: new GeoJSON(), + strategy: bboxStrategy, + }); + + const vecLayer = new VectorLayer({ + source: source, + }); + + const wms_map = new Map({ + target: mapElement.current, + layers: [ + new TileLayer({ + source: new OSM(), + }), + vecLayer, + ], + view: new View({ + projection: "EPSG:3857", + center: center, + }), + controls: [], + }); + wms_map.getView().fit(bbox); + + const info = document.getElementById("info"); + + let currentFeature: FeatureLike | undefined; + const displayFeatureInfo = function (pixel, target) { + const feature = target.closest(".ol-control") + ? undefined + : wms_map.forEachFeatureAtPixel(pixel, function (feature) { + return feature; + }); + if (feature && info) { + info.style.left = `${pixel[0]}px`; + info.style.top = `${pixel[1]}px`; + if (feature !== currentFeature) { + info.style.visibility = "visible"; + let label = + "" + ""; + const allProps = feature.getProperties(); + for (const key in allProps) { + if ( + !["operation_", "sp_region", "price", "prosperty_"].includes( + key + ) + ) + continue; + label += ``; + } + label += "
" + "FieldValue
${key}${allProps[key]}
"; + info.innerHTML = label; + } + } else { + if (info) info.style.visibility = "hidden"; + } + currentFeature = feature; + }; + + // Interactive behavior + wms_map.on("pointermove", function (evt) { + if (evt.dragging && info) { + info.style.visibility = "hidden"; + currentFeature = undefined; + return; + } + const pixel = wms_map.getEventPixel(evt.originalEvent); + displayFeatureInfo(pixel, evt.originalEvent.target); + }); + + wms_map.on("click", function (evt) { + displayFeatureInfo(evt.pixel, evt.originalEvent.target); + }); + wms_map.getTargetElement().addEventListener("pointerleave", function () { + currentFeature = undefined; + if (info) info.style.visibility = "hidden"; + }); + + setVectorRef(vecLayer); + setMap(wms_map); + } + }, [layerWMS]); + + useEffect(() => { + if (layerWMS !== undefined) { + // Determine bounding box extent & center point from URL + let bbox = [0, 0, 0, 0]; + const entries = layerWMS.split("&"); + entries.forEach((entry) => { + if (entry.startsWith("bbox=")) { + const vals = entry.replace("bbox=", "").split(","); + bbox = vals.map((v) => parseFloat(v)); + } + }); + bbox = transformExtent(bbox, "EPSG:4326", "EPSG:3857"); + const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; + + let wms_url = layerWMS; + if ( + filterAttribute && + attributeValue != undefined && + attributeValue != "Show All" + ) { + const params = new URLSearchParams(wms_url.split("?")[1]); + params.delete("bbox"); + wms_url = `${wms_url.split("?")[0]}?${params.toString()}`; + wms_url += `&CQL_FILTER=${filterAttribute}='${attributeValue}'`; + } + + const source = new VectorSource({ + url: wms_url, + format: new GeoJSON(), + strategy: bboxStrategy, + }); + + const vecLayer = new VectorLayer({ + source: source, + }); + + if (map) { + if (vectorRef) map.removeLayer(vectorRef); + map.addLayer(vecLayer); + setVectorRef(vecLayer); + } + } + }, [layerWMS, attributeValue]); + + useEffect(() => { + if (layerWMS) { + const params = new URLSearchParams(layerWMS.split("?")[1]); + params.delete("bbox"); + params.delete("outputFormat"); + let dl_url = `${layerWMS.split("?")[0]}?${params.toString()}`; + if (attributeValue && filterAttribute) + dl_url += `&CQL_FILTER=${filterAttribute}='${attributeValue}'`; + dl_url += "&outputFormat=shape-zip"; + setLayerDL(dl_url); + } + }, [attributeValue]); + + useEffect(() => { + const values: string[] = ["Show All"]; + if (vectorRef && filterAttribute) { + vectorRef + .getSource() + .getFeatures() + .forEach((feat: any) => { + const val = feat["values_"][filterAttribute]; + if (!values.includes(val)) values.push(val); + }); + setAttributeValues(values.sort()); + } + }, [filterAttribute]); + + return (() => { + return ( + <> +
+ {layerAttributes ? ( + <> + Field Name + + + ) : ( + <> + )} + {attributeValues ? ( + <> +
+ Value + + + + ) : ( + <> + )} + {layerDL ? ( + <> +
+ Download Data + + ) : ( + <> + )} +
+
+ + ); + })(); +} diff --git a/frontend/src/components/visualizations/GeospatialVector/manifest.json b/frontend/src/components/visualizations/GeospatialVector/manifest.json new file mode 100644 index 000000000..7b4c10475 --- /dev/null +++ b/frontend/src/components/visualizations/GeospatialVector/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "geoserver-vector-viewer-component", + "version": "1.0.0", + "description": "A React component to render map services such as WFS layers found in metadata.", + "main": "GeospatialVector.tsx", + "dependencies": { + "clowder2-core": "1.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "ol": "^7.4.0" + }, + "visConfig": { + "name": "GeospatialVector", + "mainType": "application", + "mimeTypes": [ + "application/zip", + "application/x-zip", + "application/x-7z-compressed", + "multi/files-zipped" + ], + "props": { + "fileId": "string" + } + } +} diff --git a/frontend/src/visualization.config.ts b/frontend/src/visualization.config.ts index 35e8c627e..708c906b0 100644 --- a/frontend/src/visualization.config.ts +++ b/frontend/src/visualization.config.ts @@ -76,6 +76,14 @@ visComponentDefinitions.push({ component: React.createElement(registerComponent(configGeospatial)), }); +const configGeospatialVector = require("./components/visualizations/GeospatialVector/manifest.json"); +visComponentDefinitions.push({ + name: configGeospatialVector.name, + mainType: configGeospatialVector.visConfig.mainType, + mimeTypes: configGeospatialVector.visConfig.mimeTypes, + component: React.createElement(registerComponent(configGeospatialVector)), +}); + const configVega = require("./components/visualizations/CSV/manifest.json"); visComponentDefinitions.push({ name: configVega.name,