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/pages/home.jsx b/src/pages/home.jsx index c40e1ec..36dd1df 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -5,6 +5,7 @@ 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; @@ -21,11 +22,6 @@ class Home extends React.Component { searchText: "", place: {}, mapHeight: window.innerHeight - SEARCH_BAR_HEIGHT, - mapCenter: { - lat: 47.665575312188025, - lng: 9.447241869601651, - }, - mapZoom: 4, selectedCoords: undefined, searchSuggestions: [], showSearchSuggestions: false, @@ -37,6 +33,13 @@ class Home extends React.Component { 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() { @@ -51,36 +54,50 @@ class Home extends React.Component { lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy, - mapCenter: { lat: position.coords.latitude, lng: position.coords.longitude }, - mapZoom: 18, }, }); - this.updatePlaceByCoords({ lat: position.coords.latitude, lng: position.coords.longitude }, 18); + 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 () => { - const coords = this.getCoordsFromSearchText(this.state.searchText); + updatePlaceBySearch = async searchText => { + const coords = this.getCoordsFromSearchText(searchText); let place = {}; if (coords !== undefined) place = await this.getPlaceByCoords(coords); - else place = await this.getPlaceByText(this.state.searchText); + 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, - mapCenter: { - lat: place.realCoords.lat, - lng: place.realCoords.lng, - }, - mapZoom: place.zoomLevel, selectedCoords: place.realCoords, + showSearchSuggestions: false, }); }; @@ -89,16 +106,19 @@ class Home extends React.Component { * @param {{lat: number, lng: number}} coords * @param {number} zoom */ - updatePlaceByCoords = async (coords, 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, - mapCenter: { - lat: place.realCoords.lat, - lng: place.realCoords.lng, - }, - mapZoom: zoom, selectedCoords: coords, }); console.log("state in updatePlaceByCoords", this.state); @@ -143,7 +163,9 @@ class Home extends React.Component { 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}&addressdetails=1`, + `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); @@ -217,7 +239,7 @@ class Home extends React.Component { const latDiff = Math.abs(lat1 - lat2); const lngDiff = Math.abs(lng1 - lng2); const maxDiff = Math.max(latDiff, lngDiff); - const zoom = Math.round(Math.log(360 / maxDiff) / Math.log(2)); + const zoom = Math.min(Math.round(Math.log(360 / maxDiff) / Math.log(2)), 18); return zoom; } @@ -246,27 +268,28 @@ class Home extends React.Component { }; /** - * Small Compnent to interact with the leaflet map + * Small Component to interact with the leaflet map * @returns {null} */ - MapHook = ({ mapCenter, onCenterChange, mapZoom, onZoomChange, onClick }) => { + MapHook = () => { const map = useMap(); // set the center of the map to this.state.mapCenter (but let the user move it) - map.setView(mapCenter, mapZoom, { animate: true, duration: 0.2 }); + 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 => { - onClick(event.latlng); - }, - drag: () => { - console.log("drag"); - onCenterChange(map.getCenter()); + this.updatePlaceByCoords(event.latlng, this.mapZoom); }, zoom: () => { - console.log("zoom", map.getZoom()); - onCenterChange(map.getCenter()); - onZoomChange(map.getZoom()); + this.mapZoom = map.getZoom(); + }, + contextmenu: event => { + this.updatePlaceByCoords(event.latlng, 18, true); }, }); @@ -295,7 +318,7 @@ class Home extends React.Component { = 17} + visible={this.state.currentLocation.accuracy !== 0} /> - this.setState({ mapCenter: center })} - mapZoom={this.state.mapZoom} - onZoomChange={zoom => this.setState({ mapZoom: zoom })} - onClick={async coords => await this.updatePlaceByCoords(coords, this.state.mapZoom)} - /> + + this.updatePlaceBySearch()} + onSubmit={event => { + event.target.blur(); // hide keyboard TODO: this is not working yet + this.updatePlaceBySearch(this.state.searchText); + }} onClickClear={() => { - this.setState({ searchText: "", showSearchSuggestions: false }); + this.setState({ searchText: "", showSearchSuggestions: false, searchSuggestions: [] }); + }} + onSearchbarDisable={() => { + this.setState({ snapSheetToState: 0, showSearchSuggestions: false }); }} onSearchbarClear={() => { this.setState({ searchText: "", showSearchSuggestions: false }); @@ -359,7 +383,7 @@ class Home extends React.Component { title={suggestion["displayName"]} onClick={() => { this.setState({ searchText: suggestion["displayName"], showSearchSuggestions: false }); - this.updatePlaceBySearch(); + this.updatePlaceBySearch(suggestion["displayName"]); }} style={{ cursor: "pointer" }} />