@rural-environmental-registry/map_component 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +444 -0
- package/dist/constants/coordinatePanel.d.ts +1 -1
- package/dist/handlers/drawingControl.d.ts +0 -1
- package/dist/handlers/mapHandler.d.ts +8 -3
- package/dist/handlers/mapToolsHandler.d.ts +39 -0
- package/dist/handlers/measureToolsHandler.d.ts +51 -0
- package/dist/handlers/memorialButtonControl.d.ts +19 -0
- package/dist/handlers/toolsConstants.d.ts +3 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +32914 -25131
- package/dist/types/index.d.ts +69 -0
- package/dist/utils/areaCalculator.d.ts +3 -0
- package/dist/utils/drawingPathOptions.d.ts +8 -0
- package/dist/utils/geometryCalculator.d.ts +13 -0
- package/dist/utils/geometryFileParseResult.d.ts +8 -0
- package/dist/utils/memorialLayer.d.ts +3 -0
- package/dist/utils/menuHistory.d.ts +8 -0
- package/dist/utils/normalizeGeoJsonToFeatures.d.ts +2 -0
- package/dist/utils/parseGeoJsonFile.d.ts +2 -0
- package/dist/utils/parseKmlFile.d.ts +2 -0
- package/dist/utils/parseShapefileZip.d.ts +3 -0
- package/dist/utils/stableMarker.d.ts +15 -0
- package/dist/utils/validateImportedPolygonGeometry.d.ts +9 -0
- package/package.json +22 -4
- package/src/assets/icons/file-import-regular-full-gray.svg +1 -0
- package/src/assets/icons/file-lines-regular-full-gray.svg +1 -0
- package/src/assets/icons/layer-icon.svg +3 -0
- package/src/assets/icons/loading.gif +0 -0
- package/src/assets/icons/marker-icon.svg +1 -0
- package/src/assets/icons/measure-icon.svg +5 -0
- package/src/assets/layers/mapLayers.json +16 -0
- package/src/assets/main.css +187 -0
- package/src/assets/styles/element-plus.css +25 -0
- package/src/assets/styles/layers-menu.css +95 -0
- package/src/assets/styles/leaflet.css +361 -0
- package/src/components/MapDPG.vue +302 -0
- package/src/components/coordinate/CoordinatePanel.vue +1206 -0
- package/src/components/fa-icon/FontAwesomeIcon.vue +44 -0
- package/src/components/loading/Loading.vue +25 -0
- package/src/components/map/LeafletMap.vue +183 -0
- package/src/components/menu/ChildMenu.vue +112 -0
- package/src/components/menu/LayerMenu.vue +294 -0
- package/src/components/menu/ParentMenu.vue +132 -0
- package/src/constants/coordinatePanel.ts +68 -0
- package/src/handlers/constants.ts +35 -0
- package/src/handlers/drawingControl.ts +143 -0
- package/src/handlers/mapHandler.ts +124 -0
- package/src/handlers/mapToolsHandler.ts +281 -0
- package/src/handlers/measureToolsHandler.ts +424 -0
- package/src/handlers/memorialButtonControl.ts +171 -0
- package/src/handlers/toolsConstants.ts +69 -0
- package/src/index.ts +6 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/types/index.ts +260 -0
- package/src/types/json.d.ts +4 -0
- package/src/types/shpjs.d.ts +7 -0
- package/src/types/svg.d.ts +4 -0
- package/src/utils/CoordinateConverter.ts +43 -0
- package/src/utils/areaCalculator.ts +9 -0
- package/src/utils/drawingPathOptions.ts +57 -0
- package/src/utils/geometryCalculator.ts +62 -0
- package/src/utils/geometryFileParseResult.ts +5 -0
- package/src/utils/memorialLayer.ts +7 -0
- package/src/utils/menuHistory.ts +71 -0
- package/src/utils/normalizeGeoJsonToFeatures.ts +52 -0
- package/src/utils/parseGeoJsonFile.ts +23 -0
- package/src/utils/parseKmlFile.ts +31 -0
- package/src/utils/parseShapefileZip.ts +29 -0
- package/src/utils/stableMarker.ts +52 -0
- package/src/utils/validateImportedPolygonGeometry.ts +62 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import L, { Layer } from 'leaflet'
|
|
2
|
+
import { area, length } from '@turf/turf'
|
|
3
|
+
import { DrawnArea } from '../types'
|
|
4
|
+
|
|
5
|
+
export function isClosedPolygonLayer(layer: Layer): boolean {
|
|
6
|
+
if (layer instanceof L.Polygon || layer instanceof L.Rectangle) return true
|
|
7
|
+
|
|
8
|
+
const geo = (layer as L.Polyline).toGeoJSON() as GeoJSON.Feature
|
|
9
|
+
const geomType = geo.geometry?.type
|
|
10
|
+
return geomType === 'Polygon' || geomType === 'MultiPolygon'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function calculateLayerArea(layer: Layer): DrawnArea {
|
|
14
|
+
if (isClosedPolygonLayer(layer)) {
|
|
15
|
+
const drawnArea = area((layer as L.Polygon).toGeoJSON())
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
m2: drawnArea,
|
|
19
|
+
km2: drawnArea / 1_000_000,
|
|
20
|
+
ha: drawnArea / 10_000
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { m2: 0, km2: 0, ha: 0 }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function calculateLayerLength(layer: Layer): { m: number; km: number } {
|
|
28
|
+
const geo = (layer as L.Polyline).toGeoJSON() as GeoJSON.Feature
|
|
29
|
+
const geomType = geo.geometry?.type
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
geomType === 'LineString' ||
|
|
33
|
+
geomType === 'MultiLineString' ||
|
|
34
|
+
geomType === 'Polygon' ||
|
|
35
|
+
geomType === 'MultiPolygon'
|
|
36
|
+
) {
|
|
37
|
+
const km = length(geo, { units: 'kilometers' })
|
|
38
|
+
return { m: km * 1000, km }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { m: 0, km: 0 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatLengthKm(km: number): string {
|
|
45
|
+
return `${km.toLocaleString(undefined, { maximumFractionDigits: 4 })} km`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatAreaKm2(km2: number): string {
|
|
49
|
+
return `${km2.toLocaleString(undefined, { maximumFractionDigits: 6 })} km²`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatAreaHa(value: number): string {
|
|
53
|
+
return `${value.toLocaleString(undefined, { maximumFractionDigits: 4 })} ha`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatLengthValue(value: number, unit: 'm' | 'km'): string {
|
|
57
|
+
const decimals = unit === 'm' ? 2 : 4
|
|
58
|
+
const suffix = unit === 'km' ? ' km' : ' m'
|
|
59
|
+
return `${value.toLocaleString(undefined, { maximumFractionDigits: decimals })}${suffix}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { formatAreaHa as formatAreaValue }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LayerData } from '../types'
|
|
2
|
+
|
|
3
|
+
const MENU_HISTORY_KEY = 'menuHistory'
|
|
4
|
+
|
|
5
|
+
const readHistory = (): Record<string, boolean> => {
|
|
6
|
+
try {
|
|
7
|
+
const raw = window.sessionStorage.getItem(MENU_HISTORY_KEY)
|
|
8
|
+
if (!raw) return {}
|
|
9
|
+
|
|
10
|
+
const parsed: unknown = JSON.parse(raw)
|
|
11
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
12
|
+
return {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return parsed as Record<string, boolean>
|
|
16
|
+
} catch {
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const setHistory = (layer: LayerData): void => {
|
|
22
|
+
try {
|
|
23
|
+
const history = readHistory()
|
|
24
|
+
window.sessionStorage.setItem(
|
|
25
|
+
MENU_HISTORY_KEY,
|
|
26
|
+
JSON.stringify({ ...history, [layer.key]: layer.active })
|
|
27
|
+
)
|
|
28
|
+
} catch {
|
|
29
|
+
// sessionStorage indisponível ou JSON inválido — ignora persistência
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Retorna o estado salvo da camada ou `undefined` se não houver histórico. */
|
|
34
|
+
export const getHistory = (layerKey: string): boolean | undefined => {
|
|
35
|
+
try {
|
|
36
|
+
const history = readHistory()
|
|
37
|
+
const isActive = history[layerKey]
|
|
38
|
+
|
|
39
|
+
return isActive !== undefined ? isActive : undefined
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resolve `active`/`activeDefault` sem mutar o objeto original (mesma regra do fluxo legado). */
|
|
46
|
+
export const resolveLayerActiveState = (layer: LayerData, persist: boolean): LayerData => {
|
|
47
|
+
const savedActive = persist ? getHistory(layer.key) : undefined
|
|
48
|
+
|
|
49
|
+
let active = layer.active ?? false
|
|
50
|
+
let activeDefault = layer.activeDefault
|
|
51
|
+
|
|
52
|
+
if (savedActive !== undefined) {
|
|
53
|
+
active = savedActive
|
|
54
|
+
activeDefault = savedActive
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (activeDefault) {
|
|
58
|
+
active = activeDefault
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { ...layer, active, activeDefault }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Indica se a camada deve ser aplicada ao mapa na inicialização. */
|
|
65
|
+
export const shouldInitLayerOnMap = (layer: LayerData, persist: boolean): boolean => {
|
|
66
|
+
const resolved = resolveLayerActiveState(layer, persist)
|
|
67
|
+
|
|
68
|
+
if (resolved.activeDefault) return true
|
|
69
|
+
|
|
70
|
+
return persist && getHistory(layer.key) !== undefined
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection, Geometry } from 'geojson'
|
|
2
|
+
|
|
3
|
+
function isFeatureCollection(value: unknown): value is FeatureCollection {
|
|
4
|
+
return (
|
|
5
|
+
typeof value === 'object' &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
'type' in value &&
|
|
8
|
+
(value as FeatureCollection).type === 'FeatureCollection' &&
|
|
9
|
+
'features' in value &&
|
|
10
|
+
Array.isArray(value.features)
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isFeature(value: unknown): value is Feature {
|
|
15
|
+
return (
|
|
16
|
+
typeof value === 'object' &&
|
|
17
|
+
value !== null &&
|
|
18
|
+
'type' in value &&
|
|
19
|
+
(value as Feature).type === 'Feature' &&
|
|
20
|
+
'geometry' in value
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isGeometry(value: unknown): value is Geometry {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === 'object' &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
'type' in value &&
|
|
29
|
+
!('features' in value) &&
|
|
30
|
+
(value as { type: string }).type !== 'Feature'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeGeoJsonToFeatures(geojson: unknown): Feature[] {
|
|
35
|
+
if (Array.isArray(geojson)) {
|
|
36
|
+
return geojson.flatMap(item => normalizeGeoJsonToFeatures(item))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isFeatureCollection(geojson)) {
|
|
40
|
+
return geojson.features
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isFeature(geojson)) {
|
|
44
|
+
return [geojson]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isGeometry(geojson)) {
|
|
48
|
+
return [{ type: 'Feature', properties: null, geometry: geojson }]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return []
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizeGeoJsonToFeatures } from './normalizeGeoJsonToFeatures'
|
|
2
|
+
import type { GeometryFileParseResult } from './geometryFileParseResult'
|
|
3
|
+
|
|
4
|
+
export async function parseGeoJsonFile(file: File): Promise<GeometryFileParseResult> {
|
|
5
|
+
const fileName = file.name.toLowerCase()
|
|
6
|
+
if (!fileName.endsWith('.geojson') && !fileName.endsWith('.json')) {
|
|
7
|
+
return { ok: false, error: 'The file must be a .geojson or .json file' }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const content = await file.text()
|
|
12
|
+
const parsed = JSON.parse(content) as unknown
|
|
13
|
+
const features = normalizeGeoJsonToFeatures(parsed)
|
|
14
|
+
|
|
15
|
+
if (features.length === 0) {
|
|
16
|
+
return { ok: false, error: 'The GeoJSON file contains no geometries' }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { ok: true, features }
|
|
20
|
+
} catch {
|
|
21
|
+
return { ok: false, error: 'Could not read the file. Check that it contains valid GeoJSON' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { kml } from '@tmcw/togeojson'
|
|
2
|
+
import { normalizeGeoJsonToFeatures } from './normalizeGeoJsonToFeatures'
|
|
3
|
+
import type { GeometryFileParseResult } from './geometryFileParseResult'
|
|
4
|
+
|
|
5
|
+
export async function parseKmlFile(file: File): Promise<GeometryFileParseResult> {
|
|
6
|
+
const fileName = file.name.toLowerCase()
|
|
7
|
+
if (!fileName.endsWith('.kml')) {
|
|
8
|
+
return { ok: false, error: 'The file must be a .kml file' }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const content = await file.text()
|
|
13
|
+
const xmlDoc = new DOMParser().parseFromString(content, 'text/xml')
|
|
14
|
+
const parserError = xmlDoc.querySelector('parsererror')
|
|
15
|
+
|
|
16
|
+
if (parserError) {
|
|
17
|
+
return { ok: false, error: 'Could not read the file. Check that it contains valid KML' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const geojson = kml(xmlDoc)
|
|
21
|
+
const features = normalizeGeoJsonToFeatures(geojson)
|
|
22
|
+
|
|
23
|
+
if (features.length === 0) {
|
|
24
|
+
return { ok: false, error: 'The KML file contains no geometries' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { ok: true, features }
|
|
28
|
+
} catch {
|
|
29
|
+
return { ok: false, error: 'Could not read the file. Check that it contains valid KML' }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import shp from 'shpjs'
|
|
2
|
+
import { normalizeGeoJsonToFeatures } from './normalizeGeoJsonToFeatures'
|
|
3
|
+
import type { GeometryFileParseResult } from './geometryFileParseResult'
|
|
4
|
+
|
|
5
|
+
export type ShapefileParseResult = GeometryFileParseResult
|
|
6
|
+
|
|
7
|
+
export async function parseShapefileZip(file: File): Promise<ShapefileParseResult> {
|
|
8
|
+
const fileName = file.name.toLowerCase()
|
|
9
|
+
if (!fileName.endsWith('.zip')) {
|
|
10
|
+
return { ok: false, error: 'The file must be a .zip containing the shapefile' }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const buffer = await file.arrayBuffer()
|
|
15
|
+
const geojson = await shp(buffer)
|
|
16
|
+
const features = normalizeGeoJsonToFeatures(geojson)
|
|
17
|
+
|
|
18
|
+
if (features.length === 0) {
|
|
19
|
+
return { ok: false, error: 'The shapefile contains no geometries' }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { ok: true, features }
|
|
23
|
+
} catch {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: 'Could not read the file. Check that the .zip contains valid .shp, .shx and .dbf files'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FeatureGroup, LatLngExpression, Map, Marker, MarkerOptions } from 'leaflet'
|
|
2
|
+
import type { default as LeafletType } from 'leaflet'
|
|
3
|
+
|
|
4
|
+
export type StableMarkerOptions = MarkerOptions & Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
/** Opções padrão para evitar drift de divIcon durante zoom no Leaflet. */
|
|
7
|
+
export const STABLE_MARKER_DEFAULTS: StableMarkerOptions = {
|
|
8
|
+
pane: 'markerPane',
|
|
9
|
+
riseOnHover: false,
|
|
10
|
+
zoomAnimation: false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cria marcador com opções estáveis para ícones customizados (divIcon).
|
|
15
|
+
* Consumidores podem passar `icon` e metadados de domínio (ex.: layerCode, rules).
|
|
16
|
+
*/
|
|
17
|
+
export function createStableMarker(
|
|
18
|
+
leaflet: typeof LeafletType,
|
|
19
|
+
latlng: LatLngExpression,
|
|
20
|
+
options: StableMarkerOptions = {}
|
|
21
|
+
): Marker {
|
|
22
|
+
return leaflet.marker(latlng, {
|
|
23
|
+
...STABLE_MARKER_DEFAULTS,
|
|
24
|
+
...options
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reposiciona marcadores após zoom (workaround para desalinhamento de divIcon).
|
|
30
|
+
* Retorna função de cleanup para remover o listener.
|
|
31
|
+
*/
|
|
32
|
+
export function bindMarkerZoomStability(map: Map, layerGroup: FeatureGroup): () => void {
|
|
33
|
+
const handler = (): void => {
|
|
34
|
+
const stabilize = (layer: any): void => {
|
|
35
|
+
if (typeof layer.getLatLng === 'function' && typeof layer.setLatLng === 'function') {
|
|
36
|
+
const latlng = layer.getLatLng()
|
|
37
|
+
if (latlng) {
|
|
38
|
+
layer.setLatLng(latlng)
|
|
39
|
+
}
|
|
40
|
+
} else if (typeof layer.eachLayer === 'function') {
|
|
41
|
+
layer.eachLayer(stabilize)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
layerGroup.eachLayer(stabilize)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
map.on('zoomend', handler)
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
map.off('zoomend', handler)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { area, booleanValid, kinks } from '@turf/turf'
|
|
2
|
+
import type { Feature, MultiPolygon, Polygon, Position } from 'geojson'
|
|
3
|
+
|
|
4
|
+
export type ImportedPolygonValidationResult =
|
|
5
|
+
| { ok: true; feature: Feature<Polygon | MultiPolygon> }
|
|
6
|
+
| { ok: false; error: string }
|
|
7
|
+
|
|
8
|
+
const COORD_TOLERANCE = 1e-9
|
|
9
|
+
|
|
10
|
+
function coordsEqual(a: Position, b: Position): boolean {
|
|
11
|
+
return Math.abs(a[0] - b[0]) <= COORD_TOLERANCE && Math.abs(a[1] - b[1]) <= COORD_TOLERANCE
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isRingClosed(ring: Position[]): boolean {
|
|
15
|
+
if (ring.length < 4) return false
|
|
16
|
+
return coordsEqual(ring[0], ring[ring.length - 1])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateRingsClosed(geometry: Polygon | MultiPolygon): boolean {
|
|
20
|
+
if (!Array.isArray(geometry?.coordinates)) return false
|
|
21
|
+
const polygons = geometry.type === 'Polygon' ? [geometry.coordinates] : geometry.coordinates
|
|
22
|
+
return polygons.every(polygon => polygon.every(ring => isRingClosed(ring)))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateImportedPolygonGeometry(features: Feature[]): ImportedPolygonValidationResult {
|
|
26
|
+
try {
|
|
27
|
+
if (features.length !== 1) {
|
|
28
|
+
return { ok: false, error: 'The file must contain exactly one geometry' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const feature = features[0]
|
|
32
|
+
const geometry = feature.geometry
|
|
33
|
+
|
|
34
|
+
if (!geometry || (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon')) {
|
|
35
|
+
return { ok: false, error: 'Geometry must be Polygon or MultiPolygon' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!validateRingsClosed(geometry)) {
|
|
39
|
+
return { ok: false, error: 'Polygon ring is not closed or is invalid' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!booleanValid(geometry)) {
|
|
43
|
+
return { ok: false, error: 'Invalid geometry' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (kinks(geometry).features.length > 0) {
|
|
47
|
+
return { ok: false, error: 'Geometry has self-intersection' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (area(geometry) <= 0) {
|
|
51
|
+
return { ok: false, error: 'Polygon has zero area' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { ok: true, feature: feature as Feature<Polygon | MultiPolygon> }
|
|
55
|
+
} catch {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error:
|
|
59
|
+
'An error occurred while validating the geometry. Ensure the file contains valid coordinates.'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|