From 2dd2f2111c27e438510b7d19fa4f97c22bd78294 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 31 Jan 2020 17:12:19 +0100 Subject: [PATCH] Further work on polygons. Multipolygons still fail. --- package.json | 1 + src/database.js | 7 ++--- src/map.js | 34 ++++++++------------ src/resultsview.js | 10 +++--- src/userquerywidget.js | 70 ++++++++++++++++++++++++++++++++++++++++-- yarn.lock | 5 +++ 6 files changed, 94 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index ce6206b..352e585 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "leaflet": "^1.6.0", "lodash": "^4.17.15", "object-hash": "^2.0.1", + "point-in-polygon": "^1.0.1", "prop-types": "^15.6.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/src/database.js b/src/database.js index 340d3fc..e5e83da 100644 --- a/src/database.js +++ b/src/database.js @@ -49,13 +49,10 @@ export function regexp_match(string, regex) { return string.match(regex) != null; } -export function is_in_polygon(x, y, poly) { - return false; -} - export function is_in_geo_polygon_from_store(lat, long, polygon_hash) { const poly = get_polygon_from_store(polygon_hash); - return is_in_polygon(lat, long, poly); + var inside = require('point-in-polygon'); + return inside([lat, long], poly); } // Digikam stores its tree of tags as individual tags, diff --git a/src/map.js b/src/map.js index 0874132..6a00eaf 100644 --- a/src/map.js +++ b/src/map.js @@ -10,6 +10,11 @@ import "leaflet/dist/leaflet.css" import L from 'leaflet'; export function MapView(props) { + // This is bad React design: this component holds state which the parent needs (the Leaflet map object). + // Normally we'd make such an object in the parent and pass it as a prop. However, the object cannot + // be created without a div being present for it to reside in, so we have a chicken-egg problem. + // We solve it by loading the map after mounting this component, then passing a handle to the parent. + const { onMapChange } = props; const [map, setMap] = useState(null); const [camera, setCameraNoPush] = useState([51.505, -0.09, 13]); //lat, long, zoom @@ -35,6 +40,14 @@ export function MapView(props) { } )); }, []) + + // Notify parent if map object changes. + useEffect(() => { + if (onMapChange) { + onMapChange(map); + } + }, map); + // Update camera state of this component when map changes. useEffect(() => { if (map != null) { @@ -47,24 +60,6 @@ export function MapView(props) { } }, [map]) - function showNominatimResult(result) { - var corner1 = L.latLng(result.boundingbox[0], result.boundingbox[2]); - var corner2 = L.latLng(result.boundingbox[1], result.boundingbox[3]); - var bounds = L.latLngBounds(corner1, corner2); - map.flyToBounds(bounds); - } - - function onSearch(query) { - fetch("http://nominatim.openstreetmap.org/search?polygon_geojson=1&polygon_threshold=0.001&format=json&limit=5&q=" + query) - .then(res => res.json()) - .then(jsonres => { - //console.log("Nominatim result: ", jsonres); - if (Array.isArray(jsonres) && jsonres.length > 0) { - showNominatimResult(jsonres[0]); - } - }); - } - const style = { width: "100%", height: "600px", @@ -72,9 +67,6 @@ export function MapView(props) { return ( <> - - -
diff --git a/src/resultsview.js b/src/resultsview.js index 0a95297..05c794c 100644 --- a/src/resultsview.js +++ b/src/resultsview.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Box from '@material-ui/core/Box'; @@ -10,7 +10,7 @@ import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; import { GridGallery } from './gridgallery.js'; -import { MapView } from './map.js'; +import { create_map, MapView } from './map.js'; const useStyles = makeStyles(theme => ({ root: { @@ -107,9 +107,9 @@ function TabPanel(props) { export function ResultsView(props) { const classes = useStyles(); const { photos, albums, tags } = props; - const [ activeIndex, setActiveIndex ] = React.useState(0); + const [ activeIndex, setActiveIndex ] = useState(0); - var _ = require('lodash'); + const _ = require('lodash'); function tabProps(index) { return { @@ -120,7 +120,7 @@ export function ResultsView(props) { const handleChange = (e, newindex) => { setActiveIndex(newindex); } - + return (
diff --git a/src/userquerywidget.js b/src/userquerywidget.js index 5688206..b71c122 100644 --- a/src/userquerywidget.js +++ b/src/userquerywidget.js @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import Switch from '@material-ui/core/Switch'; import Box from '@material-ui/core/Box'; @@ -21,9 +21,13 @@ import { MuiPickersUtilsProvider, DateTimePicker } from "@material-ui/pickers"; import DateFnsUtils from '@date-io/date-fns'; import { format } from 'date-fns'; +import L from 'leaflet'; import { makeStyles } from '@material-ui/core/styles'; +import { MapView, create_map } from './map.js'; +import { SearchBar } from './searchbar.js'; + import { filter_is_const_false, ConstFilter, LogicalOperatorFilter, MatchingFilter, ResultTypeEnum, LogicalOperatorEnum, MatchTypeEnum, NegationFilter, TimeFilterTypeEnum, @@ -53,13 +57,75 @@ const useStyles = makeStyles(theme => ({ margined: { margin: "6px", }, + mapcontainer: { + width: "600px", + }, })); export function EditLocationFilterExpression(props) { const { onChange, filter } = props; + const [map, setMap] = useState(null); + const [proposal, setProposal] = useState(filter.polygon); + const [polyLayers, setPolyLayers] = useState([]); + const classes = useStyles(); + + const _ = require('lodash'); + + function updateProposal(result) { + // TODO: handle multi-polies + const polygon = result.geojson.coordinates[0].map(longlat => [ longlat[1], longlat[0] ]); + + // Show the polyline on the map + polyLayers.forEach(layer => { + map.removeLayer(layer); + }); + var polyline = L.polyline(polygon, { color: 'blue' }).addTo(map); + map.flyToBounds(polyline.getBounds()); + setPolyLayers([polyline]); + + // Update the proposed polygon + console.log("Updated proposal:", result); + setProposal(polygon); + } + + function onSearch(query) { + if (map == null) { + return; + } + fetch("http://nominatim.openstreetmap.org/search?polygon_geojson=1&polygon_threshold=0.001&format=json&limit=5&q=" + query) + .then(res => res.json()) + .then(jsonres => { + //console.log("Nominatim result: ", jsonres); + if (Array.isArray(jsonres) && jsonres.length > 0) { + updateProposal(jsonres[0]); + } + }); + } + + function handleUseProposal() { + var new_filter = _.cloneDeep(filter); + new_filter.polygon = proposal; + onChange(new_filter); + } + + // This component is a bit tricky. MapView is not very React-y because it manages its + // own state inside (this is due to how Leaflet is integrated). + // We don't want to render a new MapView whenever our filter changes. + // Therefore we ensure that filter changes don't trigger a new MapView render. return ( - TODO + <> + + + + + + + + + Polygon with {filter.polygon.length} points. + + ); } diff --git a/yarn.lock b/yarn.lock index 0acfb3f..a5764ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7782,6 +7782,11 @@ pnp-webpack-plugin@1.5.0: dependencies: ts-pnp "^1.1.2" +point-in-polygon@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.0.1.tgz#d59b64e8fee41c49458aac82b56718c5957b2af7" + integrity sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc= + popper.js@^1.14.1: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"