diff --git a/.gitignore b/.gitignore
index 7bf522f..cf228b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ Thumbs.db
# Production build
www/
+
+# Pagekite for localhost tunneling (https://pagekite.net/)
+pagekite.py
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 5e81ea1..a40ebed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,9 +14,11 @@
"framework7": "^7.0.9",
"framework7-icons": "^5.0.5",
"framework7-react": "^7.0.9",
+ "leaflet": "^1.9.3",
"material-icons": "^1.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-leaflet": "^4.2.0",
"skeleton-elements": "^4.0.1",
"swiper": "^8.4.5"
},
@@ -2214,6 +2216,16 @@
"node": ">= 8"
}
},
+ "node_modules/@react-leaflet/core": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
+ "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -6150,6 +6162,11 @@
"node": ">=8"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
+ "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -8137,6 +8154,19 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/react-leaflet": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.0.tgz",
+ "integrity": "sha512-9d8T7hzYrQA5GLe3vn0qtRLJzQKgjr080NKa45yArGwuSl1nH/6aK9gp7DeYdktpdO1vKGSUTGW5AsUS064X0A==",
+ "dependencies": {
+ "@react-leaflet/core": "^2.1.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
diff --git a/package.json b/package.json
index 9b07485..7085956 100644
--- a/package.json
+++ b/package.json
@@ -29,9 +29,11 @@
"framework7": "^7.0.9",
"framework7-icons": "^5.0.5",
"framework7-react": "^7.0.9",
+ "leaflet": "^1.9.3",
"material-icons": "^1.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-leaflet": "^4.2.0",
"skeleton-elements": "^4.0.1",
"swiper": "^8.4.5"
},
diff --git a/public/img/LocationMarker.png b/public/img/LocationMarker.png
new file mode 100644
index 0000000..413a763
Binary files /dev/null and b/public/img/LocationMarker.png differ
diff --git a/public/img/OwnLocationMarker.png b/public/img/OwnLocationMarker.png
new file mode 100644
index 0000000..7c24645
Binary files /dev/null and b/public/img/OwnLocationMarker.png differ
diff --git a/src/components/AccuracyCircle.jsx b/src/components/AccuracyCircle.jsx
new file mode 100644
index 0000000..af0f076
--- /dev/null
+++ b/src/components/AccuracyCircle.jsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Circle } from "react-leaflet";
+
+class AccuracyCircle extends React.Component {
+ render() {
+ if (
+ this.props.visible === false ||
+ this.props.center === undefined ||
+ this.props.center === null ||
+ this.props.center.lat === undefined ||
+ this.props.center.lng === undefined ||
+ this.props.radius === undefined ||
+ this.props.radius === null
+ ) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+}
+
+// define the types of the properties that are passed to the component
+AccuracyCircle.prototype.props = /** @type { {
+ visible: boolean,
+ center: { lat: number, lng: number } | undefined,
+ radius: number | undefined,
+} } */ ({});
+
+export default AccuracyCircle;
diff --git a/src/components/LocationMarker.jsx b/src/components/LocationMarker.jsx
new file mode 100644
index 0000000..47e21cb
--- /dev/null
+++ b/src/components/LocationMarker.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { Marker } from "react-leaflet";
+import L from "leaflet";
+
+class LocationMarker extends React.Component {
+ markerIcon = L.icon({
+ iconUrl: this.props.iconUrl,
+ iconSize: [this.props.size.width, this.props.size.height],
+ // Center of the icon:
+ iconAnchor: [this.props.anchor?.x || this.props.size.width / 2, this.props.anchor?.y || this.props.size.height / 2],
+ });
+
+ render() {
+ if (
+ this.props.position === undefined ||
+ this.props.position === null ||
+ this.props.position.lat === undefined ||
+ this.props.position.lng === undefined ||
+ !this.props.visible
+ ) {
+ return null;
+ }
+
+ return ;
+ }
+}
+
+export default LocationMarker;
diff --git a/src/components/OutlinePolygon.jsx b/src/components/OutlinePolygon.jsx
new file mode 100644
index 0000000..3bdb6ec
--- /dev/null
+++ b/src/components/OutlinePolygon.jsx
@@ -0,0 +1,86 @@
+import React from "react";
+import { Polygon } from "react-leaflet";
+
+class OutlinePolygon extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ polyLatLngs: [],
+ };
+ }
+
+ async componentDidUpdate(prevProps) {
+ if (this.props.placeName === prevProps.placeName) return;
+ this.setState({ polyLatLngs: [] });
+
+ // fetch the polygon outline from the OpenStreetMap API (Nominatim)
+ // this is not done in the main component because loading the polygon outline
+ // takes more time than the normal API call
+ const response = await fetch(
+ // eslint-disable-next-line max-len
+ `https://nominatim.openstreetmap.org/search?q=${this.props.placeName}&format=json&limit=1&polygon_geojson=1&polygon_threshold=0.0005`,
+ );
+ // check if the response is suited for a polygon outline
+ const data = await response.json();
+ if (
+ data[0]?.geojson?.coordinates === undefined ||
+ data[0]?.geojson?.type === undefined ||
+ data[0]?.boundingbox === undefined ||
+ (data[0]?.geojson?.type !== "Polygon" && data[0]?.geojson?.type !== "MultiPolygon") ||
+ (data[0]?.class !== "boundary" && data[0]?.class !== "place" && data[0]?.class !== "landuse")
+ ) {
+ this.setState({ polyLatLngs: [] });
+ return;
+ }
+
+ const latLngs = this.convertGeoJsonCoordsToLeafletLatLng(data[0].geojson.coordinates);
+ this.setState({ polyLatLngs: latLngs });
+ }
+
+ /**
+ * Converts the coordinates of a GeoJSON polygon to the format used by Leaflet
+ * @param {number[]} coords The coordinates of the GeoJSON polygon
+ * @returns {number[]} The coordinates for the Leaflet polygon
+ */
+ convertGeoJsonCoordsToLeafletLatLng = coords => {
+ if (coords === undefined) return [];
+
+ const transformCoords = coords => {
+ if (
+ Array.isArray(coords) &&
+ coords.length === 2 &&
+ typeof coords[0] === "number" &&
+ typeof coords[1] === "number"
+ ) {
+ return { lat: coords[1], lng: coords[0] };
+ }
+ return coords.map(transformCoords);
+ };
+
+ const latLngs = transformCoords(coords);
+
+ return latLngs;
+ };
+
+ render() {
+ if (this.state.polyLatLngs.length === 0) return null;
+
+ return (
+
+ );
+ }
+}
+
+// define the types of the properties that are passed to the component
+OutlinePolygon.prototype.props = /** @type { {
+ placeName: string,
+} } */ ({});
+
+export default OutlinePolygon;
diff --git a/src/components/SnappingSheet.jsx b/src/components/SnappingSheet.jsx
new file mode 100644
index 0000000..27e6c8d
--- /dev/null
+++ b/src/components/SnappingSheet.jsx
@@ -0,0 +1,151 @@
+import React from "react";
+import { Sheet } from "framework7-react";
+
+class SnappingSheet extends React.Component {
+ dragStartPositionY = 0;
+ sheetScrollAreaRef = React.createRef();
+ isMoving = false;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ sheetOpened: true,
+ sheetHeight: this.props.snapHeightStates[this.props.currentState || 0],
+ sheetHeightTransitionStyle: "0.3s ease-in-out",
+ };
+ }
+
+ /**
+ * Adds touch and mouse listeners to the sheet
+ */
+ componentDidMount() {
+ // add touch listeners to the sheet
+ this.sheetScrollAreaRef.current.addEventListener("touchstart", this.dragStart, { passive: false });
+ this.sheetScrollAreaRef.current.addEventListener("touchmove", this.dragMove, { passive: false });
+ this.sheetScrollAreaRef.current.addEventListener("touchend", this.dragEnd, { passive: false });
+
+ // add mouse listeners to the sheet
+ this.sheetScrollAreaRef.current.addEventListener("mousedown", this.dragStart, { passive: false });
+ window.addEventListener("mousemove", this.dragMove, { passive: false });
+ window.addEventListener("mouseup", this.dragEnd, { passive: false });
+
+ // let the parent know that the sheet is snapped to the current state (to clear the state)
+ if (this.props.currentState !== undefined) this.props.snappedToHeight(this.props.currentState);
+ }
+
+ /**
+ * Removes touch and mouse listeners from the sheet
+ */
+ componentWillUnmount() {
+ // remove touch listeners from the sheet
+ this.sheetScrollAreaRef.current.removeEventListener("touchstart", this.dragStart);
+ this.sheetScrollAreaRef.current.removeEventListener("touchmove", this.dragMove);
+ this.sheetScrollAreaRef.current.removeEventListener("touchend", this.dragEnd);
+
+ // remove mouse listeners from the sheet
+ this.sheetScrollAreaRef.current.removeEventListener("mousedown", this.dragStart);
+ window.removeEventListener("mousemove", this.dragMove);
+ window.removeEventListener("mouseup", this.dragEnd);
+ }
+
+ /**
+ * Snaps the sheet to a given state if the state is defined
+ */
+ componentDidUpdate() {
+ if (this.props.currentState !== undefined) {
+ this.setState({
+ sheetHeightTransitionStyle: "0.3s ease-in-out",
+ sheetHeight: this.props.snapHeightStates[this.props.currentState],
+ });
+ this.props.snappedToHeight(this.props.snapHeightStates[this.props.currentState]);
+ }
+ }
+
+ /**
+ * Starts the dragging process
+ * @param {Event} event
+ */
+ dragStart = event => {
+ this.dragStartPositionY = event.touches ? event.touches[0].clientY : event.clientY;
+ this.isMoving = true;
+ this.setState({ sheetHeightTransitionStyle: "0s" });
+ };
+
+ /**
+ * Moves the sheet (param is a touch event or a mouse event)
+ * @param { Event } event
+ */
+ dragMove = event => {
+ event.preventDefault();
+
+ if (!this.isMoving) return;
+
+ // check if it is a touch event or a mouse event and get the y position
+ const dragStartPositionY = event.touches ? event.touches[0].clientY : event.clientY;
+
+ // calculate the new sheet height
+ const difference = this.dragStartPositionY - dragStartPositionY;
+ const newSheetHeight = this.state.sheetHeight + difference;
+
+ // check if the new sheet height is in the allowed range
+ if (newSheetHeight < this.props.snapHeightStates[0]) {
+ this.setState({ sheetHeight: this.props.snapHeightStates[0] });
+ return;
+ }
+ if (newSheetHeight > this.props.snapHeightStates[this.props.snapHeightStates.length - 1]) {
+ this.setState({ sheetHeight: this.props.snapHeightStates[this.props.snapHeightStates.length - 1] });
+ return;
+ }
+
+ // set the new sheet height and update the touch start position
+ this.setState({ sheetHeight: newSheetHeight });
+ this.dragStartPositionY = dragStartPositionY;
+ };
+
+ /**
+ * Ends the dragging process
+ */
+ dragEnd = () => {
+ this.isMoving = false;
+
+ // snap the sheet to the nearest height state
+ const closestSheetHeightState = this.props.snapHeightStates.reduce((prev, curr) => {
+ return Math.abs(curr - this.state.sheetHeight) < Math.abs(prev - this.state.sheetHeight) ? curr : prev;
+ });
+
+ // transition to the new sheet height
+ this.setState({ sheetHeightTransitionStyle: "0.3s ease-out", sheetHeight: closestSheetHeightState });
+ this.props.snappedToHeight(closestSheetHeightState);
+ };
+
+ render() {
+ if (this.props.children === undefined) {
+ return null;
+ }
+
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+// define the types of the properties that are passed to the component
+SnappingSheet.prototype.props = /** @type { {
+ children: React.ReactNode
+ snapHeightStates: number[],
+ currentState: number | undefined,
+ snappedToHeight: (height: number) => void
+} } */ ({});
+
+export default SnappingSheet;
diff --git a/src/components/app.jsx b/src/components/app.jsx
index 42fd95f..c94b501 100644
--- a/src/components/app.jsx
+++ b/src/components/app.jsx
@@ -2,7 +2,7 @@ import React from "react";
import process from "node:process";
-import { App, Block, Navbar, Page, Panel, View, Views } from "framework7-react";
+import { App, View, Views } from "framework7-react";
import routes from "../js/routes";
import store from "../js/store";
@@ -27,16 +27,6 @@ class MyApp extends React.Component {
render() {
return (
- {/* Left panel with cover effect*/}
-
-
-
-
- Left panel content goes here
-
-
-
-
{/* Views/Tabs container */}
diff --git a/src/js/routes.js b/src/js/routes.js
index 70ce699..07d141f 100644
--- a/src/js/routes.js
+++ b/src/js/routes.js
@@ -1,10 +1,15 @@
import Home from "../pages/home.jsx";
+import Impressum from "../pages/impressum.jsx";
var routes = [
{
path: "/",
component: Home,
},
+ {
+ path: "/impressum",
+ component: Impressum,
+ },
];
export default routes;
diff --git a/src/pages/home.jsx b/src/pages/home.jsx
index 0ae74a5..36dd1df 100644
--- a/src/pages/home.jsx
+++ b/src/pages/home.jsx
@@ -1,19 +1,406 @@
import React from "react";
-import { Page, Navbar, NavLeft, Link } from "framework7-react";
+import { Page, Searchbar, List, BlockTitle, Button, ListItem } from "framework7-react";
+import { MapContainer, TileLayer, useMap, useMapEvents } from "react-leaflet";
+import "leaflet/dist/leaflet.css";
+import SnappingSheet from "../components/SnappingSheet";
+import LocationMarker from "../components/LocationMarker";
+import AccuracyCircle from "../components/AccuracyCircle";
+import OutlinePolygon from "../components/OutlinePolygon";
+
+const SEARCH_BAR_HEIGHT = 70;
class Home extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ currentLocation: {
+ lat: 47.665575312188025,
+ lng: 9.447241869601651,
+ accuracy: 0,
+ },
+ snapSheetToState: 1,
+ searchText: "",
+ place: {},
+ mapHeight: window.innerHeight - SEARCH_BAR_HEIGHT,
+ selectedCoords: undefined,
+ searchSuggestions: [],
+ showSearchSuggestions: false,
+ };
+
+ this.sheetHeightStates = [
+ SEARCH_BAR_HEIGHT,
+ window.innerHeight * 0.25 + SEARCH_BAR_HEIGHT,
+ window.innerHeight * 0.8 + SEARCH_BAR_HEIGHT,
+ ];
+ this.suggestionTimeout = undefined;
+ this.mapNeedsUpdate = false;
+ this.mapSlowAnimation = true;
+ this.mapZoom = 4;
+ this.mapCenter = {
+ lat: 47.665575312188025,
+ lng: 9.447241869601651,
+ };
+ }
+
+ componentDidMount() {
+ // fire a resize event to fix leaflets map sizing bug
+ window.dispatchEvent(new Event("resize"));
+
+ // get the current location
+ navigator.geolocation.getCurrentPosition(position => {
+ console.log(position);
+ this.setState({
+ currentLocation: {
+ lat: position.coords.latitude,
+ lng: position.coords.longitude,
+ accuracy: position.coords.accuracy,
+ },
+ });
+ this.updatePlaceByCoords({ lat: position.coords.latitude, lng: position.coords.longitude }, 18, true);
+ });
+
+ // update the current location every 5 seconds
+ this.currentLocationInterval = setInterval(() => {
+ navigator.geolocation.getCurrentPosition(position => {
+ this.setState({
+ currentLocation: {
+ lat: position.coords.latitude,
+ lng: position.coords.longitude,
+ accuracy: position.coords.accuracy,
+ },
+ });
+ });
+ }, 5000);
+ }
+
+ /**
+ * Search for a place by text or coordinates
+ * @param {string} searchText
+ */
+ updatePlaceBySearch = async searchText => {
+ const coords = this.getCoordsFromSearchText(searchText);
+
+ let place = {};
+ if (coords !== undefined) place = await this.getPlaceByCoords(coords);
+ else place = await this.getPlaceByText(searchText);
+
+ console.log("place in updatePlaceBySearch", place);
+
+ if (place === undefined) return;
+ this.mapNeedsUpdate = true;
+ this.mapZoom = place.zoomLevel;
+ this.mapCenter = {
+ lat: place.realCoords.lat,
+ lng: place.realCoords.lng,
+ };
+ this.setState({
+ place: place,
+ snapSheetToState: 1,
+ selectedCoords: place.realCoords,
+ showSearchSuggestions: false,
+ });
+ };
+
+ /**
+ * Update the place by given coordinates
+ * @param {{lat: number, lng: number}} coords
+ * @param {number} zoom
+ */
+ updatePlaceByCoords = async (coords, zoom, updateMap = false) => {
+ if (updateMap) {
+ this.mapNeedsUpdate = true;
+ this.mapZoom = zoom;
+ this.mapCenter = {
+ lat: coords.lat,
+ lng: coords.lng,
+ };
+ }
+ const place = await this.getPlaceByCoords(coords, zoom);
+ this.setState({
+ place: place,
+ snapSheetToState: 1,
+ selectedCoords: coords,
+ });
+ console.log("state in updatePlaceByCoords", this.state);
+ };
+
+ /**
+ * Get coordinates from a string
+ * @param {string} text
+ * @returns {{lat: number, lng: number} | undefined} undefined if no coordinates were found
+ */
+ getCoordsFromSearchText = text => {
+ const coordinateRegex = /^(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)$/;
+ coordinateRegex.test(text);
+ const match = text.match(coordinateRegex);
+ if (!match) return undefined;
+ const lat = parseFloat(match[1]);
+ const lng = parseFloat(match[3]);
+ console.log(`lat: ${lat}, lng: ${lng}`);
+ return { lat: lat, lng: lng };
+ };
+
+ /**
+ * Get a place from nominatim by text
+ * @param {string} searchText
+ * @returns Promise
+ */
+ getPlaceByText = async searchText => {
+ const response = await fetch(
+ `https://nominatim.openstreetmap.org/search?q=${searchText}&format=json&addressdetails=1&limit=1&extratags=1`,
+ );
+ const data = await response.json();
+ if (data[0] === undefined) return;
+ const place = this.getPlaceByNominatimData(data[0]);
+ return place;
+ };
+
+ /**
+ * Get a place from nominatim by coordinates
+ * @param {{lat: number, lng: number}} coords
+ * @returns {Promise}
+ */
+ getPlaceByCoords = async (coords, zoom = 20) => {
+ const response = await fetch(
+ // eslint-disable-next-line max-len
+ `https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.lat}&lon=${coords.lng}&extratags=1&zoom=${
+ zoom + 1
+ }&addressdetails=1`,
+ );
+ const data = await response.json();
+ console.log(data);
+ if (data === undefined) return;
+ const place = this.getPlaceByNominatimData(data);
+ if (place?.realCoords?.lat === undefined || place?.realCoords?.lng === undefined) {
+ place.realCoords = coords;
+ }
+ return place;
+ };
+
+ /**
+ * Get a place from nominatim response data
+ * @param {any} placeData
+ * @returns {any}
+ */
+ getPlaceByNominatimData = placeData => {
+ if (placeData === undefined) return;
+ // console.log(placeData);
+ let place = {
+ name: placeData?.display_name || "Unknown location",
+ address: {
+ amenity: placeData?.address?.amenity || "",
+ city: placeData?.address?.city || "",
+ cityDistrict: placeData?.address?.city_district || "",
+ municipality: placeData?.address?.municipality || "",
+ country: placeData?.address?.country || "",
+ countryCode: placeData?.address?.country_code || "",
+ neighbourhood: placeData?.address?.neighbourhood || "",
+ postcode: placeData?.address?.postcode || "",
+ road: placeData?.address?.road || "",
+ houseNumber: placeData?.address?.house_number || "",
+ state: placeData?.address?.state || "",
+ suburb: placeData?.address?.suburb || "",
+ },
+ type: placeData?.type || "",
+ importance: placeData?.importance ? parseFloat(placeData?.lat) : 0,
+ osmId: placeData?.osm_id || 0,
+ realCoords: {
+ lat: placeData?.lat ? parseFloat(placeData?.lat) : undefined,
+ lng: placeData?.lon ? parseFloat(placeData?.lon) : undefined,
+ },
+ userInputCoords: {
+ lat: 0,
+ lng: 0,
+ },
+ zoomLevel: this.getZoomByBoundingBox(placeData?.boundingbox) || 10,
+ searchedByCoords: false,
+ searchedByCurrentLocation: false,
+ searchedByPlace: false,
+ searchedByAddress: false,
+ };
+ // console.log(place);
+ return place;
+ };
+
+ /**
+ * Get the zoom level by a given bounding box
+ * @param {number[] | string[] | undefined} boundingbox
+ * @returns {number | undefined}
+ * @see https://wiki.openstreetmap.org/wiki/Zoom_levels
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels
+ */
+ getZoomByBoundingBox(boundingbox) {
+ if (boundingbox === undefined) return undefined;
+
+ const lat1 = parseFloat(`${boundingbox[0]}`);
+ const lat2 = parseFloat(`${boundingbox[1]}`);
+ const lng1 = parseFloat(`${boundingbox[2]}`);
+ const lng2 = parseFloat(`${boundingbox[3]}`);
+ const latDiff = Math.abs(lat1 - lat2);
+ const lngDiff = Math.abs(lng1 - lng2);
+ const maxDiff = Math.max(latDiff, lngDiff);
+ const zoom = Math.min(Math.round(Math.log(360 / maxDiff) / Math.log(2)), 18);
+ return zoom;
+ }
+
+ /**
+ * Get the search suggestions from nominatim
+ * @param {string} searchText
+ * @returns {Promise}
+ */
+ updateSearchSuggestions = async searchText => {
+ if (searchText.trim().length < 3) {
+ this.setState({ searchSuggestions: [] });
+ return;
+ }
+ clearTimeout(this.suggestionTimeout);
+ this.suggestionTimeout = setTimeout(async () => {
+ const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${searchText}&format=json&limit=5`);
+ const data = await response.json();
+ const searchSuggestions = data.map(place => {
+ const placeData = {
+ displayName: place?.display_name || "Unknown location",
+ };
+ return placeData;
+ });
+ this.setState({ searchSuggestions });
+ }, 200);
+ };
+
+ /**
+ * Small Component to interact with the leaflet map
+ * @returns {null}
+ */
+ MapHook = () => {
+ const map = useMap();
+
+ // set the center of the map to this.state.mapCenter (but let the user move it)
+ if (this.mapNeedsUpdate) {
+ map.flyTo(this.mapCenter, this.mapZoom, { animate: true, duration: this.mapSlowAnimation ? 4 : 1 });
+ this.mapNeedsUpdate = false;
+ this.mapSlowAnimation = false;
+ }
+
+ useMapEvents({
+ click: event => {
+ this.updatePlaceByCoords(event.latlng, this.mapZoom);
+ },
+ zoom: () => {
+ this.mapZoom = map.getZoom();
+ },
+ contextmenu: event => {
+ this.updatePlaceByCoords(event.latlng, 18, true);
+ },
+ });
+
+ return null;
+ };
+
render() {
+ // address object -> string
+ let address = "";
+ if (this.state.place.address !== undefined) {
+ Object.values(this.state.place.address).forEach(value => {
+ if (value !== undefined && value !== "") address += value + ", ";
+ });
+ }
+
return (
- {/* Top Navbar */}
+
+
+
+
+
+
+
+
-
-
-
-
-
+ this.setState({ snapSheetToState: undefined })}
+ >
+ {
+ this.setState({ snapSheetToState: 2, showSearchSuggestions: true });
+ }}
+ placeholder="Place, address, or coordinates (lat, lng)"
+ onChange={event => {
+ this.setState({ searchText: event.target.value });
+ this.updateSearchSuggestions(event.target.value);
+ }}
+ onSubmit={event => {
+ event.target.blur(); // hide keyboard TODO: this is not working yet
+ this.updatePlaceBySearch(this.state.searchText);
+ }}
+ onClickClear={() => {
+ this.setState({ searchText: "", showSearchSuggestions: false, searchSuggestions: [] });
+ }}
+ onSearchbarDisable={() => {
+ this.setState({ snapSheetToState: 0, showSearchSuggestions: false });
+ }}
+ onSearchbarClear={() => {
+ this.setState({ searchText: "", showSearchSuggestions: false });
+ }}
+ onClickDisable={() => {
+ this.setState({ snapSheetToState: 0, showSearchSuggestions: false });
+ }}
+ />
+
+ {this.state.showSearchSuggestions &&
+ this.state.searchSuggestions.map((suggestion, index) => {
+ return (
+ {
+ this.setState({ searchText: suggestion["displayName"], showSearchSuggestions: false });
+ this.updatePlaceBySearch(suggestion["displayName"]);
+ }}
+ style={{ cursor: "pointer" }}
+ />
+ );
+ })}
+
- {/* Page content */}
+ {this.state.place.name}
+ {address}
+
+
);
}
diff --git a/src/pages/impressum.jsx b/src/pages/impressum.jsx
new file mode 100644
index 0000000..b48ebe8
--- /dev/null
+++ b/src/pages/impressum.jsx
@@ -0,0 +1,129 @@
+import React from "react";
+import { Page, Block, BlockTitle, Link } from "framework7-react";
+
+class Impressum extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
+
+ About Know-Where-You-Go
+
+
+
+ {" "}
+ Know-Where-You-Go ist eine Webanwendung, die Ihnen hilft, herauszufinden, wo Sie sind und wohin Sie gehen.
+
+
+ Es basiert auf dem OpenStreetMap-Projekt und verwendet
+ die OpenStreetMap-API, um die Daten zu erhalten.
+
+
+ Die Anwendung ist open source und kann auf{" "}
+ GitHub gefunden werden.
+
+ Die App wird von Studenten der DHBW Friedrichshafen entwickelt:
+
+
+ Baldur Siegel
+
+
+ Henry Schuler
+
+
+ Florian Herkommer
+
+
+ Florian Glaser
+
+
+ Johannes Brandenburger
+
+
+ David Felder
+
+
+ Lukas Braun
+
+
+ Phillipp Patzelt
+
+
+
+
+ Impressum
+
+
+ Kontakt
+
+
+ Angaben gemäß § 5 TMG
+
+ Florian Glaser
+
+
+ Fallenbrunnen 2
+ 88045 Friedrichshafen
+ Telefon: 49-123456789
+
+ E-Mail:{" "}
+ glaser.florian-it20@it.dhbw-ravensburg.de
+
+
+ Haftungsausschluss:
+
+
+ Haftung für Inhalte
+
+ Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und
+ Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7
+ Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis
+ 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde
+ Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
+ Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen
+ bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer
+ konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese
+ Inhalte umgehend entfernen.
+
+ Haftung für Links
+
+ Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben.
+ Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten
+ Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten
+ wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum
+ Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist
+ jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von
+ Rechtsverletzungen werden wir derartige Links umgehend entfernen.
+
+ Urheberrecht
+
+ Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
+ Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
+ Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
+ Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit
+ die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet.
+ Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine
+ Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von
+ Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
+
+
+
+ {/* // prettier-ignore */}
+ Website Impressum erstellt durch
+ impressum-generator.de
+ {" "}
+ von der{" "}
+
+ Kanzlei Hasselbach
+
+
+
+ );
+ }
+}
+
+export default Impressum;