feat: support relocating
This commit is contained in:
parent
80278d4c21
commit
e80311a1b4
|
@ -7,8 +7,8 @@
|
||||||
<list default="true" id="3c7078e7-6f30-4d92-9696-11496f9e6dff" name="Changes" comment="">
|
<list default="true" id="3c7078e7-6f30-4d92-9696-11496f9e6dff" name="Changes" comment="">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/api/click.js" beforeDir="false" afterPath="$PROJECT_DIR$/api/click.js" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/api/click.js" beforeDir="false" afterPath="$PROJECT_DIR$/api/click.js" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/App.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/App.js" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/Networking.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/Networking.js" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/Networking.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/Networking.js" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/SimulateClick.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/SimulateClick.js" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/UMap.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/UMap.js" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/UMap.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/UMap.js" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
@ -83,7 +83,8 @@
|
||||||
<workItem from="1700814808669" duration="47000" />
|
<workItem from="1700814808669" duration="47000" />
|
||||||
<workItem from="1700822693197" duration="9493000" />
|
<workItem from="1700822693197" duration="9493000" />
|
||||||
<workItem from="1701004587189" duration="3578000" />
|
<workItem from="1701004587189" duration="3578000" />
|
||||||
<workItem from="1701042611758" duration="3355000" />
|
<workItem from="1701042611758" duration="10222000" />
|
||||||
|
<workItem from="1701056208994" duration="2524000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|
23
api/click.js
23
api/click.js
|
@ -1,9 +1,22 @@
|
||||||
const getArgs=(s)=>s.split('@@')
|
import {post} from "../src/Networking";
|
||||||
|
|
||||||
export default function handler(req,res){
|
export default function handler(req,res){
|
||||||
const {body}=req;
|
const pts=JSON.parse(req.body);
|
||||||
res.status(200).json({
|
const latRange=pts.map((row)=>row[0]),
|
||||||
log: `Method: click\nArgs: ${body}\nStatus: returned the raw polylines`,
|
lonRange=pts.map((row)=>row[1]);
|
||||||
multipolyline: body,
|
const minlon=Math.min(...lonRange),minlat=Math.min(...latRange),
|
||||||
|
maxlon=Math.max(...lonRange),maxlat=Math.max(...latRange);
|
||||||
|
const request_uri=`https://www.overpass-api.de/api/interpreter?data=[out:json];node[highway](${minlat},${minlon},${maxlat},${maxlon});out;`;
|
||||||
|
const fetch_debug_response= fetch(request_uri).then((response)=>{
|
||||||
|
return response.json();
|
||||||
});
|
});
|
||||||
|
fetch_debug_response.then((debug_response)=>{
|
||||||
|
res.status(200).json({
|
||||||
|
log: `Method: click\nArgs: ${pts}\nStatus: requested "${request_uri}", got response ${JSON.stringify(debug_response.elements)}`,
|
||||||
|
multipolyline: JSON.stringify(pts),
|
||||||
|
});
|
||||||
|
}).catch(e=>{
|
||||||
|
res.status(500);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
export function post(type,method,args) {
|
export function post(type,method,args) {
|
||||||
let res = '';
|
|
||||||
let ft = async () => {
|
let ft = async () => {
|
||||||
let response;
|
let response;
|
||||||
if(type==='GET'){
|
if(type==='GET'){
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import {Button, IconButton, ButtonGroup, Menu, MenuItem, Stack} from "@mui/joy";
|
import {Button, IconButton, ButtonGroup, Menu, MenuItem, Stack} from "@mui/joy";
|
||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Add} from "@mui/icons-material";
|
import {Add, LocationOn} from "@mui/icons-material";
|
||||||
|
|
||||||
export default function SimulateClick({isCandEmpty}) {
|
export default function SimulateClick({isLocated,isCandEmpty,relocator}) {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup buttonFlex={1} aria-label="flex button group" sx={{zIndex: 'modal'}}>
|
<ButtonGroup buttonFlex={1} aria-label="flex button group" sx={{zIndex: 'modal'}}>
|
||||||
|
<Button disabled={isLocated} variant={'solid'} color={'warning'} startDecorator={<LocationOn />} onClick={relocator}>Relocate</Button>
|
||||||
<Button disabled={isCandEmpty} variant={'solid'} color={'primary'} startDecorator={<Add />}>Add</Button>
|
<Button disabled={isCandEmpty} variant={'solid'} color={'primary'} startDecorator={<Add />}>Add</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
|
126
src/UMap.js
126
src/UMap.js
|
@ -5,26 +5,31 @@ import {useMapEvents} from 'react-leaflet/hooks';
|
||||||
import {post} from './Networking';
|
import {post} from './Networking';
|
||||||
import {Autocomplete, CircularProgress, Sheet} from "@mui/joy";
|
import {Autocomplete, CircularProgress, Sheet} from "@mui/joy";
|
||||||
import SimulateClick from "./SimulateClick";
|
import SimulateClick from "./SimulateClick";
|
||||||
|
import {NearMe} from "@mui/icons-material";
|
||||||
|
|
||||||
|
const defaultZoomLevel=16;
|
||||||
|
const defaultLocationName = 'Apple Park';
|
||||||
|
const defaultLocation = [37.334835049999995, -122.01139165956805];
|
||||||
|
|
||||||
const AppleParkLoc=[37.334835049999995,-122.01139165956805];
|
|
||||||
class Markers extends Component {
|
class Markers extends Component {
|
||||||
|
// A location is a [lat,lon] pair.
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {markers: [], candMarkers: [], candEmpty: true, polylines: []};
|
this.state = {markers: [], candMarkers: [], candEmpty: true, polylines: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const fillBlueOptions = { fillColor: 'blue' };
|
const fillBlueOptions = {fillColor: 'blue'};
|
||||||
const pl=this.state.polylines;
|
const pl = this.state.polylines;
|
||||||
const mks=this.state.markers.map((p, i) => (
|
const mks = this.state.markers.map((p, i) => (
|
||||||
<Marker key={`m`+i} interactive={false} position={[p[0], p[1]]} opacity={1.0} />
|
<Marker key={`m` + i} interactive={false} position={[p[0], p[1]]} opacity={1.0}/>
|
||||||
));
|
));
|
||||||
const cmks=this.state.candMarkers.map((p, i) => (
|
const cmks = this.state.candMarkers.map((p, i) => (
|
||||||
<Marker key={`c`+i} interactive={false} position={[p[0], p[1]]} opacity={0.5} />
|
<Marker key={`c` + i} interactive={false} position={[p[0], p[1]]} opacity={0.5}/>
|
||||||
));
|
));
|
||||||
return [
|
return [
|
||||||
...mks.concat(cmks),
|
...mks.concat(cmks),
|
||||||
pl&&pl.length>1?<Polyline pathOptions={fillBlueOptions} positions={pl} />:<div/>
|
pl && pl.length > 1 ? <Polyline pathOptions={fillBlueOptions} positions={pl}/> : <div/>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +50,7 @@ class Markers extends Component {
|
||||||
candEmpty: false,
|
candEmpty: false,
|
||||||
polylines: prev.polylines,
|
polylines: prev.polylines,
|
||||||
}));
|
}));
|
||||||
this.getFocus();
|
this.getFocus(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMarkers() {
|
clearMarkers() {
|
||||||
|
@ -58,7 +63,7 @@ class Markers extends Component {
|
||||||
this.getFocus();
|
this.getFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCandMarkers(){
|
clearCandMarkers() {
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
markers: prev.markers,
|
markers: prev.markers,
|
||||||
candMarkers: [],
|
candMarkers: [],
|
||||||
|
@ -68,20 +73,23 @@ class Markers extends Component {
|
||||||
this.getFocus();
|
this.getFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
getFocus() {
|
getFocus(virtual=false) {
|
||||||
let currentFocus=[];
|
let currentFocus = [];
|
||||||
if (this.state.candMarkers.length) {
|
if (this.state.candMarkers.length) {
|
||||||
currentFocus=this.state.candMarkers.at(-1);
|
currentFocus = this.state.candMarkers.at(-1);
|
||||||
} else if (this.state.markers.length) {
|
} else if (this.state.markers.length) {
|
||||||
currentFocus=this.state.markers.at(-1);
|
currentFocus = this.state.markers.at(-1);
|
||||||
} else {
|
} else {
|
||||||
currentFocus=AppleParkLoc;
|
currentFocus = defaultLocation;
|
||||||
}
|
}
|
||||||
this.props.focusUpdater(currentFocus);
|
this.props.focusUpdater(currentFocus);
|
||||||
|
if(virtual){
|
||||||
|
this.props.locator(currentFocus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flushPolylines(pl){
|
flushPolylines(pl) {
|
||||||
this.setState((prev)=>({
|
this.setState((prev) => ({
|
||||||
markers: prev.markers,
|
markers: prev.markers,
|
||||||
candMarkers: prev.candMarkers,
|
candMarkers: prev.candMarkers,
|
||||||
candEmpty: prev.candEmpty,
|
candEmpty: prev.candEmpty,
|
||||||
|
@ -91,18 +99,21 @@ class Markers extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function MapClickHandler({mks}) {
|
function MapClickHandler({mks,focusUpdater,locator,locker}) {
|
||||||
const map = useMapEvents({
|
const map = useMapEvents({
|
||||||
click: (e) => {
|
click: (e) => {
|
||||||
map.locate();
|
map.locate();
|
||||||
const lat = e.latlng.lat, lng = e.latlng.lng;
|
const {lat,lng}=e.latlng;
|
||||||
console.info(`Clicking on ${lat} ${lng}`);
|
console.info(`Clicking on ${lat} ${lng}`);
|
||||||
mks.current.addMarker(lat, lng);
|
mks.current.addMarker(lat, lng);
|
||||||
post('POST','click', mks.current.state.markers).then((response)=>{
|
post('POST', 'click', mks.current.state.markers).then((response) => {
|
||||||
// TODO: real functionality
|
// TODO: real functionality
|
||||||
const pl=JSON.parse(response.multipolyline);
|
const pl = JSON.parse(response.multipolyline);
|
||||||
mks.current.flushPolylines(pl);
|
mks.current.flushPolylines(pl);
|
||||||
}).catch((e)=>{
|
focusUpdater([lat,lng]);
|
||||||
|
locator([lat,lng]);
|
||||||
|
locker(true);
|
||||||
|
}).catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
// location.reload();
|
// location.reload();
|
||||||
});
|
});
|
||||||
|
@ -115,11 +126,21 @@ function MapClickHandler({mks}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationSearch = ({mks}) => {
|
const LocationSearch = ({mks, focus, nb}) => {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [suggestedLocations, setSuggestedLocations] = useState([]);
|
const [suggestedLocations, setSuggestedLocations] = useState([]);
|
||||||
const [controller, setController] = useState(new AbortController());
|
const [controller, setController] = useState(new AbortController());
|
||||||
|
const [everywhere,setEverywhere]=useState(false);
|
||||||
|
|
||||||
|
const SearchLogo=()=>{
|
||||||
|
const cf=()=>setEverywhere(!everywhere);
|
||||||
|
if(everywhere){
|
||||||
|
return <SearchIcon onClick={cf}/>;
|
||||||
|
} else{
|
||||||
|
return <NearMe onClick={cf}/>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
|
@ -137,7 +158,7 @@ const LocationSearch = ({mks}) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?format=json&q=${v}`,
|
`https://nominatim.openstreetmap.org/search?format=json&q=${v}${everywhere?'':' '+focus[0]+','+focus[1]}`,
|
||||||
{signal: newController.signal}
|
{signal: newController.signal}
|
||||||
);
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
@ -164,40 +185,69 @@ const LocationSearch = ({mks}) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Autocomplete sx={{zIndex: 'snackbar'}} loading={loading} loadingText={"Searching..."} startDecorator={<SearchIcon/>}
|
return <Autocomplete sx={{zIndex: 'snackbar'}} loading={loading} loadingText={"Searching..."}
|
||||||
placeholder={'Find a location...'} onInputChange={handleSearch}
|
startDecorator={<SearchLogo/>}
|
||||||
options={suggestedLocations} isOptionEqualToValue={(option, value) => option.value === value.value} endDecorator={
|
placeholder={everywhere?'Search everywhere...':`Search near${nb}`} onInputChange={handleSearch}
|
||||||
|
options={suggestedLocations}
|
||||||
|
isOptionEqualToValue={(option, value) => option.value === value.value} endDecorator={
|
||||||
loading ? (
|
loading ? (
|
||||||
<CircularProgress size="sm" sx={{bgcolor: 'background.surface'}}/>
|
<CircularProgress size="sm" sx={{bgcolor: 'background.surface'}}/>
|
||||||
) : null
|
) : null
|
||||||
}/>;
|
}/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChangeView({center,zoom}){
|
function ChangeView({center, zoom}) {
|
||||||
const map=useMap();
|
const map = useMap();
|
||||||
map.setView(center,zoom);
|
map.setView(center, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UMap() {
|
export default function UMap() {
|
||||||
const markersRef = useRef(null);
|
const markersRef = useRef(null);
|
||||||
const [focus, setFocus]=useState(AppleParkLoc);
|
const [focus, setFocus] = useState(defaultLocation);
|
||||||
const zoom=16;
|
const [located,setLocated]=useState(true);
|
||||||
const sf=(a)=>{setFocus(a);console.log(`triggered focus update, new focus is ${focus}`);};
|
const [locatedFocus,setLocatedFocus]=useState(defaultLocation);
|
||||||
|
const [nearbyName, setNearbyName] = useState(' ' + defaultLocationName);
|
||||||
|
const [zoom,setZoom] = useState(defaultZoomLevel);
|
||||||
|
const sf = (a) => {
|
||||||
|
setFocus(a);
|
||||||
|
console.log(`triggered focus update, new focus is ${focus}`);
|
||||||
|
const ft = fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${a[0]}&lon=${a[1]}&zoom=${zoom}&addressdetails=0`).then(response => response.json());
|
||||||
|
ft.then((response) => setNearbyName(' ' + response.name)).catch(() => setNearbyName('by'));
|
||||||
|
};
|
||||||
|
const relo=()=> {
|
||||||
|
sf(locatedFocus);
|
||||||
|
setLocated(true);
|
||||||
|
};
|
||||||
|
const ViewportChange = () => {
|
||||||
|
const map = useMapEvents({
|
||||||
|
dragend: (e) => {
|
||||||
|
const center = e.target.getCenter();
|
||||||
|
sf([center.lat, center.lng]);
|
||||||
|
setLocated(false);
|
||||||
|
},
|
||||||
|
zoomend: (e)=>{
|
||||||
|
setZoom(e.target.getZoom());
|
||||||
|
sf(focus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<MapContainer style={{height: '100vh', width: '100vw',}} sx={{ zIndex: 'fab'}} center={focus} zoom={zoom}
|
<MapContainer style={{height: '100vh', width: '100vw',}} sx={{zIndex: 'fab'}} center={focus} zoom={zoom}
|
||||||
scrollWheelZoom={false}>
|
scrollWheelZoom={false}>
|
||||||
<ChangeView center={focus} zoom={zoom}/>
|
<ChangeView center={focus} zoom={zoom}/>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
<Markers ref={markersRef} focusUpdater={sf}/>
|
<Markers ref={markersRef} focusUpdater={sf} locator={setLocatedFocus} />
|
||||||
<MapClickHandler mks={markersRef}/>
|
<MapClickHandler mks={markersRef} focusUpdater={setFocus} locator={setLocatedFocus} locker={setLocated}/>
|
||||||
|
<ViewportChange/>
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
<Sheet sx={{position: 'absolute', top: '20px', right: '10vw', zIndex: 'modal'}}>
|
<Sheet sx={{position: 'absolute', top: '20px', right: '10vw', zIndex: 'modal'}}>
|
||||||
<SimulateClick isCandEmpty={markersRef.current?markersRef.current.state.candEmpty:true} />
|
<SimulateClick isLocated={located} relocator={relo} isCandEmpty={markersRef.current ? markersRef.current.state.candEmpty : true}/>
|
||||||
<LocationSearch mks={markersRef}/>
|
<LocationSearch nb={nearbyName} mks={markersRef} focus={focus}/>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue