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 */
|
||||
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 SaveIcon from '@mui/icons-material/Save'
|
||||
import { usePageContext } from '../Page'
|
||||
@@ -9,32 +9,8 @@ import { EditDataType, Editable, Reference } from '@/shared/types'
|
||||
import { useState, useEffect, Fragment } from 'react'
|
||||
import { referenceValidator } from '@/shared/validators/validator'
|
||||
import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes'
|
||||
import { isPointInBox } from '@/util/isPointInBox'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
import { boundingBoxSplit, isPointInBoxes } from '@/util/isPointInBox'
|
||||
import { OutOfBoundsWarningModal, OutOfBoundsWarningModalState } from './OutOfBoundsWarningModal'
|
||||
|
||||
export const WriteButton = <T,>({
|
||||
onWrite,
|
||||
@@ -47,7 +23,13 @@ export const WriteButton = <T,>({
|
||||
useDetailContext<T>()
|
||||
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 = () => {
|
||||
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
|
||||
const isCoordinateOutOfBounds = (dec_lat: number, dec_long: number, country: string): boolean => {
|
||||
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
|
||||
@@ -133,11 +115,12 @@ export const WriteButton = <T,>({
|
||||
return (
|
||||
<>
|
||||
<OutOfBoundsWarningModal
|
||||
isOpen={outOfBoundsWarningOpen}
|
||||
isOpen={warningModalOpen}
|
||||
onAnswer={isOkay => {
|
||||
setOutOfBoundsWarningOpen(false)
|
||||
setWarningModalOpen(false)
|
||||
if (isOkay) save()
|
||||
}}
|
||||
state={warningModalState}
|
||||
/>
|
||||
<Button
|
||||
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 }
|
||||
if (
|
||||
!isCoordinateOutOfBounds(localityObject['dec_lat'], localityObject['dec_long'], localityObject['country'])
|
||||
) {
|
||||
if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) {
|
||||
save()
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -4,23 +4,25 @@ import { countryPolygons } from '../../country_data/countryPolygons.ts'
|
||||
import L, { LatLngExpression } from 'leaflet'
|
||||
import '../../styles/SingleLocalityMap.css'
|
||||
import northarrow from './images/north-arrow.png'
|
||||
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes.ts'
|
||||
|
||||
interface Props {
|
||||
dec_lat: number | undefined
|
||||
dec_long: number | undefined
|
||||
decLat?: number
|
||||
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 [map, setMap] = useState<L.Map | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || !dec_lat || !dec_long) return
|
||||
if (!mapRef.current || !decLat || !decLong) return
|
||||
|
||||
const mapInstance = L.map(mapRef.current, { maxZoom: 16 })
|
||||
setMap(mapInstance)
|
||||
|
||||
const coords: LatLngExpression = [dec_lat, dec_long]
|
||||
const coords: LatLngExpression = [decLat, decLong]
|
||||
mapInstance.setView(coords, 4)
|
||||
|
||||
// ---- Base maps ----
|
||||
@@ -80,15 +82,29 @@ export const SingleLocalityMap = ({ dec_lat, dec_long }: Props) => {
|
||||
return () => {
|
||||
mapInstance.remove()
|
||||
}
|
||||
}, [dec_lat, dec_long])
|
||||
}, [decLat, decLong])
|
||||
|
||||
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()
|
||||
|
||||
newMarker.addLayer(
|
||||
L.circleMarker([dec_lat, dec_long], {
|
||||
L.circleMarker([decLat, decLong], {
|
||||
radius: 4,
|
||||
color: '#db2c2c',
|
||||
fillColor: '#d95050',
|
||||
@@ -105,7 +121,7 @@ export const SingleLocalityMap = ({ dec_lat, dec_long }: Props) => {
|
||||
return () => {
|
||||
map.removeLayer(newMarker)
|
||||
}
|
||||
}, [map, dec_lat, dec_long])
|
||||
}, [map, decLat, decLong])
|
||||
|
||||
document.title = 'Map'
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -48,6 +48,13 @@ function main() {
|
||||
right: null,
|
||||
}
|
||||
|
||||
const halvedBounds = {
|
||||
top: null,
|
||||
left: null,
|
||||
bottom: null,
|
||||
right: null,
|
||||
}
|
||||
|
||||
feature.geometry.coordinates.forEach(poly => {
|
||||
polygons.push(
|
||||
poly[0].map(coord => {
|
||||
@@ -58,13 +65,21 @@ function main() {
|
||||
if (!bounds.bottom) bounds.bottom = lat
|
||||
if (!bounds.left) bounds.left = long
|
||||
if (!bounds.right) bounds.right = long
|
||||
|
||||
bounds.top = Math.min(bounds.top, 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]
|
||||
})
|
||||
)
|
||||
@@ -73,7 +88,10 @@ function main() {
|
||||
let country = feature.properties.name
|
||||
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) + ';'
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
import { CountryBoundingBox } from '@/country_data/countryBoundingBoxes'
|
||||
|
||||
// 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 => {
|
||||
if (dec_lat < box.top) return false
|
||||
if (dec_lat > box.bottom) return false
|
||||
if (dec_long < box.left) return false
|
||||
if (dec_long > box.right) return false
|
||||
export const isPointInBoxes = (dec_lat: number, dec_long: number, boxes: CountryBoundingBox[]): boolean => {
|
||||
for (const box of boxes) {
|
||||
if (dec_lat < box.top) continue
|
||||
if (dec_lat > box.bottom) continue
|
||||
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