Skip to content

Commit

Permalink
Add map with layers and controls (#55)
Browse files Browse the repository at this point in the history
* Add map with layers and controls

* Add blank line and spaces

* Only use relevant attributes of Bohrung

* Remove Info button and show info in selection

* Rename GB-Nummer to Grundbuchnummer

* Add help for search and map

Co-authored-by: MiraGeowerkstatt <[email protected]>

Co-authored-by: Daniel Jovanovic <[email protected]>

close #15
  • Loading branch information
MiraGeowerkstatt authored Jun 30, 2022
1 parent 4a853a8 commit dbcf525
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 66 deletions.
22 changes: 22 additions & 0 deletions docs/articles/einsteigsseite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Einstiegsseite
Die Einstiegsseite besteht aus einer Karte und einer Suchmaske.

## Karte
Zu Beginn zeigt die Karte alle Bohrungen im Kanton Solothurn an. Um in der Karte zu navigieren stehen die folgenden Werkzeuge zur Verfügung:
* Zoom In / Zoom Out ![Zoom In / Zoom Out Icon](../images/zoom-icon.png)
* Ansicht gesamter Kanton ![Ansicht gesamter Kanton Icon](../images/all-out-icon.png)
* Zurück zur letzten Ansicht ![Zurück zur letzten Ansicht Icon](../images/back-icon.png)

Das Verschieben des Kartenausschnitts geschieht durch Klicken und Ziehen im Kartenfenster, ohne dass ein Werkzeug ausgewählt werden muss.

Durch Klicken auf eine Bohrung werden die wichtigsten Informationen in einem Popup-Fenster angezeigt, ohne dass ein Werkzeug ausgewählt werden muss.

## Suchmaske
Die Suchmaske besteht aus fünf Eingabefeldern nach denen die Bohrungen bzw. Standorte durchsucht werden können:
* Gemeinde
* Grundbuchnummer(n)
* Bezeichnung
* Erstellungsdatum
* Mutationsdatum

Die gefundenen Standorte werden in einer Tabelle unterhalb der Suchmaske angezeigt. Im Kartenfenster werden nur noch die Bohrungen der gefundenen Standorte angezeigt, der Kartenausschnitt passt sich automatisch den Suchergebnissen an.
1 change: 0 additions & 1 deletion docs/articles/intro.md

This file was deleted.

4 changes: 2 additions & 2 deletions docs/articles/toc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
- name: Einleitung
href: intro.md
- name: Einstiegsseite
href: einsteigsseite.md
Binary file added docs/images/all-out-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/back-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/zoom-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/ClientApp/cypress/integration/home.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ describe("Home page tests", () => {
cy.get("div[name=home-container]").should("contain", "Suchresultate");
cy.get("tbody").children().should("have.length", 1);
});

it("Show map", function () {
cy.visit("/");
cy.get("div[class=map-container]").should("be.visible");
});
});
14 changes: 6 additions & 8 deletions src/ClientApp/src/components/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function Home() {
const [bezeichnung, setBezeichnung] = useState("");
const [erstellungsDatum, setErstellungsDatum] = useState(null);
const [mutationsDatum, setMutationsDatum] = useState(null);
const [hasResults, setHasResults] = useState(false);
const [hasFilters, setHasFilters] = useState(false);

const search = (event) => {
event.preventDefault();
Expand All @@ -27,10 +27,9 @@ export function Home() {
fetch("/standort" + query)
.then((response) => response.json())
.then((fetchedFeatures) => {
setHasResults(
fetchedFeatures.length > 0 &&
// at least one filter paramter is set
(gemeindenummer || gbnummer || bezeichnung || erstellungsDatum || mutationsDatum)
setHasFilters(
// at least one filter paramter is set
gemeindenummer || gbnummer || bezeichnung || erstellungsDatum || mutationsDatum
);
setStandorte(fetchedFeatures);
});
Expand All @@ -43,7 +42,6 @@ export function Home() {
.then((response) => response.json())
.then((fetchedFeatures) => {
setStandorte(fetchedFeatures);
setStandorte(fetchedFeatures);
});
}, []);

Expand Down Expand Up @@ -94,12 +92,12 @@ export function Home() {
</Paper>
</Grid>
<Grid item xs={12}>
{hasResults && standorte.lenght !== 0 && (
{hasFilters && standorte.length > 0 && (
<Paper sx={{ p: 2, display: "flex", flexDirection: "column" }}>
<SearchResults standorte={standorte} />
</Paper>
)}
{standorte.length === 0 && (
{hasFilters && standorte.length === 0 && (
<Paper sx={{ p: 2, display: "flex", flexDirection: "column" }}>
<div>Keine Resultate gefunden</div>
</Paper>
Expand Down
202 changes: 149 additions & 53 deletions src/ClientApp/src/components/MainMap.js
Original file line number Diff line number Diff line change
@@ -1,93 +1,173 @@
import React, { useState, useEffect, useRef } from "react";
import ReactDOMServer from "react-dom/server";
import AllOutIcon from "@mui/icons-material/AllOut";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import TileWMS from "ol/source/TileWMS";
import WMTS from "ol/source/WMTS";
import VectorSource from "ol/source/Vector";
import GeoJSON from "ol/format/GeoJSON";
import Feature from "ol/Feature";
import Projection from "ol/proj/Projection";
import { addProjection } from "ol/proj";
import Overlay from "ol/Overlay";
import { optionsFromCapabilities } from "ol/source/WMTS";
import { WMTSCapabilities } from "ol/format";
import { Projection, addProjection } from "ol/proj";
import { Vector } from "ol/layer";
import { Point } from "ol/geom";
import { Stroke, Style } from "ol/style";
import { Style, Circle, Fill, Stroke } from "ol/style";
import { Select } from "ol/interaction";
import { click } from "ol/events/condition";
import { ZoomToExtent, defaults as defaultControls } from "ol/control";
import { ZoomToLatest } from "./ZoomToLatestControl";
import Popup from "./Popup";
import "ol/ol.css";

export default function MainMap(props) {
const { standorte } = props;
const [map, setMap] = useState();
const [bohrungenLayer, setBohrungenLayer] = useState();
const [kantonsgrenze, setKantonsgrenze] = useState();
const [kantonsgrenzeLayer, setKantonsgrenzeLayer] = useState();
const [latestExtent, setLatestExtent] = useState();
const [doZoom, setDoZoom] = useState(true);
const [selectedFeature, setSelectedFeature] = useState();
const [popupVisible, setPopupVisible] = useState(false);
const [popup, setPopup] = useState();

const mapElement = useRef();
const mapRef = useRef();
mapRef.current = map;
mapElement.current = map;
const popupElement = useRef();

// Get Kantonsgrenze
useEffect(() => {
fetch(
"https://api3.geo.admin.ch/rest/services/api/MapServer/ch.swisstopo.swissboundaries3d-kanton-flaeche.fill/11?geometryFormat=geojson&sr=2056"
)
.then((response) => response.json())
.then((fetchedFeatures) => {
setKantonsgrenze([new GeoJSON().readFeature(fetchedFeatures.feature)]);
const handleZoomToLatestExtend = () => setDoZoom(true);
const resetZoom = () => setDoZoom(false);
const closePopup = () => setPopupVisible(false);

const defaultStyle = new Style({
image: new Circle({
radius: 4,
stroke: new Stroke({
color: [25, 118, 210, 1],
width: 2,
}),
fill: new Fill({
color: [25, 118, 210, 0.3],
}),
}),
});

const selectedStyleFunction = (feature) => {
if (selectedFeature !== feature) {
setSelectedFeature(feature);
return new Style({
image: new Circle({
radius: 5,
stroke: new Stroke({
color: [233, 197, 19, 1],
width: 5,
}),
fill: new Fill({
color: [25, 118, 210, 0.3],
}),
}),
});
}, []);
} else {
return defaultStyle;
}
};

// Initialize map on first render
useEffect(() => {
// Add custom projection for LV95
const projection = new Projection({
code: "EPSG:2056",
extent: [2485071.58, 1075346.31, 2828515.82, 1299941.79],
extent: [2572670, 1211017, 2664529, 1263980],
units: "m",
});
addProjection(projection);

const landeskarte = new TileLayer({
source: new TileWMS({
url: "https://wms.geo.admin.ch/?VERSION=1.3.0&lang=de",
params: { LAYERS: "ch.swisstopo.pixelkarte-grau", TILED: true },
serverType: "mapserver",
projection: projection,
crossOrigin: "anonymous",
// Add controls
const htmlIcon = ReactDOMServer.renderToStaticMarkup(<AllOutIcon />);
const icon = new DOMParser().parseFromString(htmlIcon, "text/html").getElementsByTagName("svg")[0];
icon.setAttribute("style", "padding-right: 2px; padding-bottom: 2px");

const controls = defaultControls().extend([
new ZoomToLatest(handleZoomToLatestExtend),
new ZoomToExtent({
label: icon,
extent: projection.getExtent(),
}),
});
]);

// Create map and feature layer
const bohrungenLayer = new Vector({
zIndex: 1,
source: new VectorSource(),
style: defaultStyle,
});

const kantonsgrenzeLayer = new Vector({
source: new VectorSource(),
style: new Style({
stroke: new Stroke({
color: "#8b0000",
width: 3,
}),
}),
});

// Create map and add layers
const initialMap = new Map({
controls: controls,
target: mapElement.current,
layers: [landeskarte, bohrungenLayer, kantonsgrenzeLayer],
layers: [bohrungenLayer],
view: new View({
projection: projection,
maxZoom: 14,
zoom: 2,
}),
});

// Add selection logic
const clearSelect = () => {
selectClick.getFeatures().clear();
setSelectedFeature(null);
popup.setPosition(null);
};

const selectClick = new Select({
condition: click,
style: selectedStyleFunction,
});
initialMap.addInteraction(selectClick);

// Clear selection on random click
initialMap.on("click", clearSelect);
initialMap.getView().on("change:resolution", resetZoom);

// Add info popup
let popup = new Overlay({
element: popupElement.current,
autoPan: true,
autoPanAnimation: { duration: 250 },
});

initialMap.addOverlay(popup);

// Save map and vector layer references to state
setMap(initialMap);
setBohrungenLayer(bohrungenLayer);
setKantonsgrenzeLayer(kantonsgrenzeLayer);
setPopup(popup);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Set Bohrungen to layer and center map around Bohrungen
// Handle asynchronous calls to layer sources
// Baselayer
useEffect(() => {
const parser = new WMTSCapabilities();
const url = "https://geo.so.ch/api/wmts/1.0.0/WMTSCapabilities.xml";
fetch(url)
.then(function (response) {
return response.text();
})
.then(function (text) {
const result = parser.read(text);
const options = optionsFromCapabilities(result, {
layer: "ch.so.agi.hintergrundkarte_sw",
});
const landeskarte = new TileLayer({
source: new WMTS(options),
zIndex: 0,
});
map && map.addLayer(landeskarte);
});
}, [map]);

// BohrungenLayer
useEffect(() => {
if (standorte && bohrungenLayer) {
let parsedFeatures;
Expand All @@ -97,8 +177,9 @@ export default function MainMap(props) {
(f) =>
new Feature({
geometry: new Point([f.geometrie.coordinates[0], f.geometrie.coordinates[1]]),
name: f.bezeichnung,
id: f.id,
Id: f.id,
Bezeichnung: f.bezeichnung,
"Standort Id": f.standortId,
})
);
} else {
Expand All @@ -110,27 +191,42 @@ export default function MainMap(props) {
})
);
if (bohrungen.length) {
map.getView().fit(bohrungenLayer.getSource().getExtent(), {
const currentExtent = bohrungenLayer.getSource().getExtent();
setLatestExtent(currentExtent);
map.getView().fit(currentExtent, {
padding: [30, 30, 30, 30],
});
}
}
}, [standorte, bohrungenLayer, map]);

// Set Kantonsgrenze to layer
// Handle event from zoom to latest control
useEffect(() => {
if (kantonsgrenze?.length) {
kantonsgrenzeLayer.setSource(
new VectorSource({
features: kantonsgrenze,
})
);
if (map && doZoom && latestExtent) {
map.getView().fit(latestExtent, {
padding: [30, 30, 30, 30],
});
}
}, [doZoom, latestExtent, map]);

// Handle event from info button control
useEffect(() => {
if (selectedFeature) {
popup && popup.setPosition(selectedFeature.values_.geometry.flatCoordinates);
popup && popup.setPositioning("top-center");
setPopupVisible(true);
}
}, [kantonsgrenzeLayer, kantonsgrenze]);
}, [map, popup, selectedFeature]);

return (
<div>
<div ref={mapElement} className="map-container"></div>
<Popup
closePopup={closePopup}
selectedFeature={selectedFeature}
popupVisible={popupVisible}
popupElement={popupElement}
></Popup>
</div>
);
}
Loading

0 comments on commit dbcf525

Please sign in to comment.