mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Fixed coordinate bounding box math
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import { Button, Modal } from '@mui/material'
|
||||||
|
import { SingleLocalityMap } from '../Map/SingleLocalityMap'
|
||||||
|
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes'
|
||||||
|
|
||||||
|
export interface OutOfBoundsWarningModalState {
|
||||||
|
decLong: number
|
||||||
|
decLat: number
|
||||||
|
boxes?: CountryBoundingBox[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onAnswer: (answer: boolean) => void
|
||||||
|
state: OutOfBoundsWarningModalState
|
||||||
|
isOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OutOfBoundsWarningModal = ({ isOpen, state, onAnswer }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} className={`modal ${isOpen ? 'open' : ''}`} onClick={() => onAnswer(false)}>
|
||||||
|
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||||
|
<h3>Coordinate warning</h3>
|
||||||
|
<p style={{ marginBottom: '2em' }}>
|
||||||
|
The coordinate selected is outside of the locality country. Do you still want to proceed?
|
||||||
|
</p>
|
||||||
|
<SingleLocalityMap decLat={state.decLat} decLong={state.decLong} boxes={state.boxes} />
|
||||||
|
<div style={{ marginTop: '1em' }}>
|
||||||
|
<Button variant="contained" onClick={() => onAnswer(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ marginLeft: '0.6em' }} variant="contained" onClick={() => onAnswer(true)}>
|
||||||
|
Proceed
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Button, Box, Typography, CircularProgress, Divider, alpha, List, ListItemText, Modal } from '@mui/material'
|
import { Button, Box, Typography, CircularProgress, Divider, alpha, List, ListItemText } from '@mui/material'
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
||||||
import SaveIcon from '@mui/icons-material/Save'
|
import SaveIcon from '@mui/icons-material/Save'
|
||||||
import { usePageContext } from '../Page'
|
import { usePageContext } from '../Page'
|
||||||
@@ -9,32 +9,8 @@ import { EditDataType, Editable, Reference } from '@/shared/types'
|
|||||||
import { useState, useEffect, Fragment } from 'react'
|
import { useState, useEffect, Fragment } from 'react'
|
||||||
import { referenceValidator } from '@/shared/validators/validator'
|
import { referenceValidator } from '@/shared/validators/validator'
|
||||||
import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes'
|
import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes'
|
||||||
import { isPointInBox } from '@/util/isPointInBox'
|
import { boundingBoxSplit, isPointInBoxes } from '@/util/isPointInBox'
|
||||||
|
import { OutOfBoundsWarningModal, OutOfBoundsWarningModalState } from './OutOfBoundsWarningModal'
|
||||||
export const OutOfBoundsWarningModal = ({
|
|
||||||
isOpen,
|
|
||||||
onAnswer,
|
|
||||||
}: {
|
|
||||||
isOpen: boolean
|
|
||||||
onAnswer: (answer: boolean) => void
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Modal open={isOpen} className={`modal ${isOpen ? 'open' : ''}`} onClick={() => onAnswer(false)}>
|
|
||||||
<div className="modal-content">
|
|
||||||
<h3>Coordinate warning</h3>
|
|
||||||
<p style={{ marginBottom: '2em' }}>
|
|
||||||
The coordinate selected is outside of the locality country. Do you still want to proceed?
|
|
||||||
</p>
|
|
||||||
<Button variant="contained" onClick={() => onAnswer(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button sx={{ marginLeft: '0.6em' }} variant="contained" onClick={() => onAnswer(true)}>
|
|
||||||
Proceed
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WriteButton = <T,>({
|
export const WriteButton = <T,>({
|
||||||
onWrite,
|
onWrite,
|
||||||
@@ -47,7 +23,13 @@ export const WriteButton = <T,>({
|
|||||||
useDetailContext<T>()
|
useDetailContext<T>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const [outOfBoundsWarningOpen, setOutOfBoundsWarningOpen] = useState(false)
|
// For coordinate out of bounds warning
|
||||||
|
const [warningModalState, setWarningModalState] = useState<OutOfBoundsWarningModalState>({
|
||||||
|
decLong: 0,
|
||||||
|
decLat: 0,
|
||||||
|
boxes: undefined,
|
||||||
|
})
|
||||||
|
const [warningModalOpen, setWarningModalOpen] = useState(false)
|
||||||
|
|
||||||
const getButtonText = () => {
|
const getButtonText = () => {
|
||||||
if (!mode.staging) return hasStagingMode ? 'Finalize entry' : 'Save changes'
|
if (!mode.staging) return hasStagingMode ? 'Finalize entry' : 'Save changes'
|
||||||
@@ -57,7 +39,7 @@ export const WriteButton = <T,>({
|
|||||||
// Checks if given coordinate is outside of country bounding box
|
// Checks if given coordinate is outside of country bounding box
|
||||||
const isCoordinateOutOfBounds = (dec_lat: number, dec_long: number, country: string): boolean => {
|
const isCoordinateOutOfBounds = (dec_lat: number, dec_long: number, country: string): boolean => {
|
||||||
if (!(country in countryBoundingBoxes)) return false
|
if (!(country in countryBoundingBoxes)) return false
|
||||||
return !isPointInBox(dec_lat, dec_long, countryBoundingBoxes[country])
|
return !isPointInBoxes(dec_lat, dec_long, boundingBoxSplit(countryBoundingBoxes[country]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/* validates all fields when entering new/editing mode
|
/* validates all fields when entering new/editing mode
|
||||||
@@ -133,11 +115,12 @@ export const WriteButton = <T,>({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OutOfBoundsWarningModal
|
<OutOfBoundsWarningModal
|
||||||
isOpen={outOfBoundsWarningOpen}
|
isOpen={warningModalOpen}
|
||||||
onAnswer={isOkay => {
|
onAnswer={isOkay => {
|
||||||
setOutOfBoundsWarningOpen(false)
|
setWarningModalOpen(false)
|
||||||
if (isOkay) save()
|
if (isOkay) save()
|
||||||
}}
|
}}
|
||||||
|
state={warningModalState}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={Object.keys(fieldsWithErrors).length > 0}
|
disabled={Object.keys(fieldsWithErrors).length > 0}
|
||||||
@@ -156,14 +139,19 @@ export const WriteButton = <T,>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string }
|
const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string }
|
||||||
if (
|
if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) {
|
||||||
!isCoordinateOutOfBounds(localityObject['dec_lat'], localityObject['dec_long'], localityObject['country'])
|
|
||||||
) {
|
|
||||||
save()
|
save()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutOfBoundsWarningOpen(true)
|
setWarningModalOpen(true)
|
||||||
|
setWarningModalState({
|
||||||
|
decLat: localityObject.dec_lat,
|
||||||
|
decLong: localityObject.dec_long,
|
||||||
|
// Guaranteed to exist by the isCoordinateOutOfBounds check above,
|
||||||
|
// as it will return false and return on no key found
|
||||||
|
boxes: boundingBoxSplit(countryBoundingBoxes[localityObject.country]),
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ import { countryPolygons } from '../../country_data/countryPolygons.ts'
|
|||||||
import L, { LatLngExpression } from 'leaflet'
|
import L, { LatLngExpression } from 'leaflet'
|
||||||
import '../../styles/SingleLocalityMap.css'
|
import '../../styles/SingleLocalityMap.css'
|
||||||
import northarrow from './images/north-arrow.png'
|
import northarrow from './images/north-arrow.png'
|
||||||
|
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes.ts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dec_lat: number | undefined
|
decLat?: number
|
||||||
dec_long: number | undefined
|
decLong?: number
|
||||||
|
boxes?: CountryBoundingBox[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingleLocalityMap = ({ dec_lat, dec_long }: Props) => {
|
export const SingleLocalityMap = ({ decLat, decLong, boxes }: Props) => {
|
||||||
const mapRef = useRef<HTMLDivElement | null>(null)
|
const mapRef = useRef<HTMLDivElement | null>(null)
|
||||||
const [map, setMap] = useState<L.Map | null>(null)
|
const [map, setMap] = useState<L.Map | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current || !dec_lat || !dec_long) return
|
if (!mapRef.current || !decLat || !decLong) return
|
||||||
|
|
||||||
const mapInstance = L.map(mapRef.current, { maxZoom: 16 })
|
const mapInstance = L.map(mapRef.current, { maxZoom: 16 })
|
||||||
setMap(mapInstance)
|
setMap(mapInstance)
|
||||||
|
|
||||||
const coords: LatLngExpression = [dec_lat, dec_long]
|
const coords: LatLngExpression = [decLat, decLong]
|
||||||
mapInstance.setView(coords, 4)
|
mapInstance.setView(coords, 4)
|
||||||
|
|
||||||
// ---- Base maps ----
|
// ---- Base maps ----
|
||||||
@@ -80,15 +82,29 @@ export const SingleLocalityMap = ({ dec_lat, dec_long }: Props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
mapInstance.remove()
|
mapInstance.remove()
|
||||||
}
|
}
|
||||||
}, [dec_lat, dec_long])
|
}, [decLat, decLong])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !dec_lat || !dec_long) return
|
if (!map || !decLat || !decLong) return
|
||||||
|
|
||||||
|
if (boxes) {
|
||||||
|
boxes.forEach(box => {
|
||||||
|
L.polygon(
|
||||||
|
[
|
||||||
|
[box.top, box.left],
|
||||||
|
[box.top, box.right],
|
||||||
|
[box.bottom, box.right],
|
||||||
|
[box.bottom, box.left],
|
||||||
|
] as LatLngExpression[],
|
||||||
|
{ color: 'blue' }
|
||||||
|
).addTo(map)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const newMarker = L.layerGroup()
|
const newMarker = L.layerGroup()
|
||||||
|
|
||||||
newMarker.addLayer(
|
newMarker.addLayer(
|
||||||
L.circleMarker([dec_lat, dec_long], {
|
L.circleMarker([decLat, decLong], {
|
||||||
radius: 4,
|
radius: 4,
|
||||||
color: '#db2c2c',
|
color: '#db2c2c',
|
||||||
fillColor: '#d95050',
|
fillColor: '#d95050',
|
||||||
@@ -105,7 +121,7 @@ export const SingleLocalityMap = ({ dec_lat, dec_long }: Props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
map.removeLayer(newMarker)
|
map.removeLayer(newMarker)
|
||||||
}
|
}
|
||||||
}, [map, dec_lat, dec_long])
|
}, [map, decLat, decLong])
|
||||||
|
|
||||||
document.title = 'Map'
|
document.title = 'Map'
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -48,6 +48,13 @@ function main() {
|
|||||||
right: null,
|
right: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const halvedBounds = {
|
||||||
|
top: null,
|
||||||
|
left: null,
|
||||||
|
bottom: null,
|
||||||
|
right: null,
|
||||||
|
}
|
||||||
|
|
||||||
feature.geometry.coordinates.forEach(poly => {
|
feature.geometry.coordinates.forEach(poly => {
|
||||||
polygons.push(
|
polygons.push(
|
||||||
poly[0].map(coord => {
|
poly[0].map(coord => {
|
||||||
@@ -58,13 +65,21 @@ function main() {
|
|||||||
if (!bounds.bottom) bounds.bottom = lat
|
if (!bounds.bottom) bounds.bottom = lat
|
||||||
if (!bounds.left) bounds.left = long
|
if (!bounds.left) bounds.left = long
|
||||||
if (!bounds.right) bounds.right = long
|
if (!bounds.right) bounds.right = long
|
||||||
|
|
||||||
bounds.top = Math.min(bounds.top, lat)
|
bounds.top = Math.min(bounds.top, lat)
|
||||||
bounds.bottom = Math.max(bounds.bottom, lat)
|
bounds.bottom = Math.max(bounds.bottom, lat)
|
||||||
|
bounds.left = Math.min(bounds.left, long)
|
||||||
|
bounds.right = Math.max(bounds.right, long)
|
||||||
|
|
||||||
|
if (!halvedBounds.top) halvedBounds.top = lat
|
||||||
|
if (!halvedBounds.bottom) halvedBounds.bottom = lat
|
||||||
|
if (!halvedBounds.right) halvedBounds.right = -180
|
||||||
|
if (!halvedBounds.left) halvedBounds.left = long
|
||||||
|
halvedBounds.top = Math.min(halvedBounds.top, lat)
|
||||||
|
halvedBounds.bottom = Math.max(halvedBounds.bottom, lat)
|
||||||
|
|
||||||
|
if (long < 0) halvedBounds.right = Math.max(halvedBounds.right, long)
|
||||||
|
else halvedBounds.left = Math.min(halvedBounds.left, long)
|
||||||
|
|
||||||
if ((long < 0 && bounds.left < 0) || (long > 0 && bounds.left > 0)) bounds.left = Math.min(bounds.left, long)
|
|
||||||
if ((long < 0 && bounds.right < 0) || (long > 0 && bounds.right > 0))
|
|
||||||
bounds.right = Math.max(bounds.right, long)
|
|
||||||
return [lat, long]
|
return [lat, long]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -73,7 +88,10 @@ function main() {
|
|||||||
let country = feature.properties.name
|
let country = feature.properties.name
|
||||||
if (country in nameMappings) country = nameMappings[country]
|
if (country in nameMappings) country = nameMappings[country]
|
||||||
|
|
||||||
boundingBoxes[country] = bounds
|
const boundsWidth = bounds.right - bounds.left
|
||||||
|
const halvedBoundsWidth = halvedBounds.right + 180 + 180 - halvedBounds.left
|
||||||
|
const box = boundsWidth < halvedBoundsWidth ? bounds : halvedBounds
|
||||||
|
boundingBoxes[country] = box
|
||||||
})
|
})
|
||||||
|
|
||||||
const polygonString = 'export const countryPolygons: number[][][] = ' + JSON.stringify(polygons) + ';'
|
const polygonString = 'export const countryPolygons: number[][][] = ' + JSON.stringify(polygons) + ';'
|
||||||
|
|||||||
@@ -1,11 +1,34 @@
|
|||||||
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes'
|
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes'
|
||||||
|
|
||||||
// Checks whether or not given latitude longitude point is within a bounding box.
|
// Checks whether or not given latitude longitude point is within a bounding box.
|
||||||
export const isPointInBox = (dec_lat: number, dec_long: number, box: CountryBoundingBox): boolean => {
|
export const isPointInBoxes = (dec_lat: number, dec_long: number, boxes: CountryBoundingBox[]): boolean => {
|
||||||
if (dec_lat < box.top) return false
|
for (const box of boxes) {
|
||||||
if (dec_lat > box.bottom) return false
|
if (dec_lat < box.top) continue
|
||||||
if (dec_long < box.left) return false
|
if (dec_lat > box.bottom) continue
|
||||||
if (dec_long > box.right) return false
|
if (dec_long < box.left) continue
|
||||||
|
if (dec_long > box.right) continue
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits bounding box that wraps around the longitude
|
||||||
|
export const boundingBoxSplit = (box: CountryBoundingBox): CountryBoundingBox[] => {
|
||||||
|
if (box.right >= box.left) return [box]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
top: box.top,
|
||||||
|
bottom: box.bottom,
|
||||||
|
left: -180,
|
||||||
|
right: box.right,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
top: box.top,
|
||||||
|
bottom: box.bottom,
|
||||||
|
left: box.left,
|
||||||
|
right: 180,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user