@rural-environmental-registry/map_component 1.1.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.
@@ -211,6 +211,7 @@ export type CoordinatePanelTexts = {
211
211
  dragShapefileZip?: string;
212
212
  shapefileZipInfo?: string;
213
213
  shapefileAppliedSuccess?: string;
214
+ geometryImportUnsupportedFormat?: string;
214
215
  placeholderLongitude?: string;
215
216
  placeholderLatitude?: string;
216
217
  placeholderAzimuth?: string;
@@ -0,0 +1,8 @@
1
+ import type { Feature } from 'geojson';
2
+ export type GeometryFileParseResult = {
3
+ ok: true;
4
+ features: Feature[];
5
+ } | {
6
+ ok: false;
7
+ error: string;
8
+ };
@@ -0,0 +1,2 @@
1
+ import type { Feature } from 'geojson';
2
+ export declare function normalizeGeoJsonToFeatures(geojson: unknown): Feature[];
@@ -0,0 +1,2 @@
1
+ import type { GeometryFileParseResult } from './geometryFileParseResult';
2
+ export declare function parseGeoJsonFile(file: File): Promise<GeometryFileParseResult>;
@@ -0,0 +1,2 @@
1
+ import type { GeometryFileParseResult } from './geometryFileParseResult';
2
+ export declare function parseKmlFile(file: File): Promise<GeometryFileParseResult>;
@@ -1,9 +1,3 @@
1
- import type { Feature } from 'geojson';
2
- export type ShapefileParseResult = {
3
- ok: true;
4
- features: Feature[];
5
- } | {
6
- ok: false;
7
- error: string;
8
- };
1
+ import type { GeometryFileParseResult } from './geometryFileParseResult';
2
+ export type ShapefileParseResult = GeometryFileParseResult;
9
3
  export declare function parseShapefileZip(file: File): Promise<ShapefileParseResult>;
@@ -1,9 +1,9 @@
1
1
  import type { Feature, MultiPolygon, Polygon } from 'geojson';
2
- export type ShapefileValidationResult = {
2
+ export type ImportedPolygonValidationResult = {
3
3
  ok: true;
4
4
  feature: Feature<Polygon | MultiPolygon>;
5
5
  } | {
6
6
  ok: false;
7
7
  error: string;
8
8
  };
9
- export declare function validateShapefileGeometry(features: Feature[]): ShapefileValidationResult;
9
+ export declare function validateImportedPolygonGeometry(features: Feature[]): ImportedPolygonValidationResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rural-environmental-registry/map_component",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Map component for Vue.js applications",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",
@@ -47,7 +47,8 @@
47
47
  "leaflet": "1.9.4",
48
48
  "leaflet-draw": "1.0.4",
49
49
  "papaparse": "5.5.3",
50
- "shpjs": "6.1.0"
50
+ "shpjs": "6.1.0",
51
+ "@tmcw/togeojson": "7.1.2"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/geojson": "7946.0.16",
@@ -320,9 +320,9 @@
320
320
  <input
321
321
  ref="shapefileInputRef"
322
322
  type="file"
323
- accept=".zip"
323
+ accept=".zip,.kml,.geojson,.json"
324
324
  class="shapefile-input-hidden"
325
- @change="handleShapefileInputChange"
325
+ @change="handleGeometryFileInputChange"
326
326
  />
327
327
  </div>
328
328
  </template>
@@ -353,7 +353,10 @@
353
353
  import MemorialButtonControl from '../../handlers/memorialButtonControl'
354
354
  import type { DescriptiveMemorial } from '../../types'
355
355
  import { parseShapefileZip } from '../../utils/parseShapefileZip'
356
- import { validateShapefileGeometry } from '../../utils/validateShapefileGeometry'
356
+ import { parseGeoJsonFile } from '../../utils/parseGeoJsonFile'
357
+ import { parseKmlFile } from '../../utils/parseKmlFile'
358
+ import { validateImportedPolygonGeometry } from '../../utils/validateImportedPolygonGeometry'
359
+ import type { GeometryFileParseResult } from '../../utils/geometryFileParseResult'
357
360
  import type { Feature, MultiPolygon, Polygon } from 'geojson'
358
361
 
359
362
  interface CSVRow {
@@ -669,18 +672,38 @@
669
672
  shapefileInputRef.value?.click()
670
673
  }
671
674
 
672
- const processShapefile = async (file: File) => {
675
+ const getGeometryFileExtension = (fileName: string): 'zip' | 'kml' | 'geojson' | null => {
676
+ const lower = fileName.toLowerCase()
677
+
678
+ if (lower.endsWith('.zip')) return 'zip'
679
+ if (lower.endsWith('.kml')) return 'kml'
680
+ if (lower.endsWith('.geojson') || lower.endsWith('.json')) return 'geojson'
681
+
682
+ return null
683
+ }
684
+
685
+ const parseGeometryFile = async (file: File): Promise<GeometryFileParseResult> => {
686
+ const extension = getGeometryFileExtension(file.name)
687
+
688
+ if (extension === 'zip') return parseShapefileZip(file)
689
+ if (extension === 'kml') return parseKmlFile(file)
690
+ if (extension === 'geojson') return parseGeoJsonFile(file)
691
+
692
+ return { ok: false, error: texts.value.geometryImportUnsupportedFormat || 'Unsupported file format' }
693
+ }
694
+
695
+ const processGeometryFile = async (file: File) => {
673
696
  emit('geometryRemoved')
674
697
  csvData.value = []
675
698
  shapefileLoaded.value = false
676
699
 
677
- const parseResult = await parseShapefileZip(file)
700
+ const parseResult = await parseGeometryFile(file)
678
701
  if (!parseResult.ok) {
679
702
  ElMessage.error(parseResult.error)
680
703
  return
681
704
  }
682
705
 
683
- const validationResult = validateShapefileGeometry(parseResult.features)
706
+ const validationResult = validateImportedPolygonGeometry(parseResult.features)
684
707
  if (!validationResult.ok) {
685
708
  ElMessage.error(validationResult.error)
686
709
  return
@@ -691,12 +714,12 @@
691
714
  ElMessage.success(texts.value.shapefileAppliedSuccess)
692
715
  }
693
716
 
694
- const handleShapefileInputChange = async (event: Event) => {
717
+ const handleGeometryFileInputChange = async (event: Event) => {
695
718
  const input = event.target as HTMLInputElement
696
719
  const file = input.files?.[0]
697
720
  if (!file) return
698
721
 
699
- await processShapefile(file)
722
+ await processGeometryFile(file)
700
723
  input.value = ''
701
724
  }
702
725
 
@@ -45,12 +45,13 @@ export const COORDINATE_PANEL_TEXTS: CoordinatePanelTexts = {
45
45
  csvColumnsInfo: 'The file must contain the columns: X, Y, AZIMUTH, DISTANCE',
46
46
  applyCsvCoordinates: 'Apply CSV Coordinates',
47
47
 
48
- shapefileUpload: 'Shapefile Upload',
49
- shapefileFileUpload: 'Shapefile Upload',
50
- dragShapefileZip: 'Drag ZIP file or click to upload',
48
+ shapefileUpload: 'Shapefile, KML and Geojson Upload',
49
+ shapefileFileUpload: 'Shapefile, KML and Geojson Upload',
50
+ dragShapefileZip: 'Drag file or click to upload',
51
51
  shapefileZipInfo:
52
- 'Upload a .zip with .shp, .shx and .dbf containing a single Polygon or MultiPolygon geometry',
53
- shapefileAppliedSuccess: 'Shapefile applied successfully',
52
+ 'Upload a .zip (shapefile), .kml, .geojson or .json with a single Polygon or MultiPolygon geometry',
53
+ shapefileAppliedSuccess: 'Geometry applied successfully',
54
+ geometryImportUnsupportedFormat: 'Unsupported file format. Use .zip, .kml, .geojson or .json',
54
55
 
55
56
  placeholderLongitude: 'e.g., -46.6333',
56
57
  placeholderLatitude: 'e.g., -23.5505',
@@ -240,6 +240,7 @@ export type CoordinatePanelTexts = {
240
240
  dragShapefileZip?: string
241
241
  shapefileZipInfo?: string
242
242
  shapefileAppliedSuccess?: string
243
+ geometryImportUnsupportedFormat?: string
243
244
  placeholderLongitude?: string
244
245
  placeholderLatitude?: string
245
246
  placeholderAzimuth?: string
@@ -0,0 +1,5 @@
1
+ import type { Feature } from 'geojson'
2
+
3
+ export type GeometryFileParseResult =
4
+ | { ok: true; features: Feature[] }
5
+ | { ok: false; error: string }
@@ -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
+ }
@@ -1,35 +1,8 @@
1
1
  import shp from 'shpjs'
2
- import type { Feature, FeatureCollection } from 'geojson'
2
+ import { normalizeGeoJsonToFeatures } from './normalizeGeoJsonToFeatures'
3
+ import type { GeometryFileParseResult } from './geometryFileParseResult'
3
4
 
4
- export type ShapefileParseResult =
5
- | { ok: true; features: Feature[] }
6
- | { ok: false; error: string }
7
-
8
- function isFeatureCollection(value: unknown): value is FeatureCollection {
9
- return (
10
- typeof value === 'object' &&
11
- value !== null &&
12
- 'type' in value &&
13
- (value as FeatureCollection).type === 'FeatureCollection'
14
- )
15
- }
16
-
17
- function normalizeToFeatures(geojson: FeatureCollection | Feature | Feature[] | FeatureCollection[]): Feature[] {
18
- if (Array.isArray(geojson)) {
19
- return geojson.flatMap((item) => {
20
- if (isFeatureCollection(item)) {
21
- return item.features
22
- }
23
- return [item as Feature]
24
- })
25
- }
26
-
27
- if (isFeatureCollection(geojson)) {
28
- return geojson.features
29
- }
30
-
31
- return [geojson as Feature]
32
- }
5
+ export type ShapefileParseResult = GeometryFileParseResult
33
6
 
34
7
  export async function parseShapefileZip(file: File): Promise<ShapefileParseResult> {
35
8
  const fileName = file.name.toLowerCase()
@@ -40,7 +13,7 @@ export async function parseShapefileZip(file: File): Promise<ShapefileParseResul
40
13
  try {
41
14
  const buffer = await file.arrayBuffer()
42
15
  const geojson = await shp(buffer)
43
- const features = normalizeToFeatures(geojson as FeatureCollection | Feature | Feature[] | FeatureCollection[])
16
+ const features = normalizeGeoJsonToFeatures(geojson)
44
17
 
45
18
  if (features.length === 0) {
46
19
  return { ok: false, error: 'The shapefile contains no geometries' }
@@ -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
+ }
@@ -1,54 +0,0 @@
1
- import { area, booleanValid, kinks } from '@turf/turf'
2
- import type { Feature, MultiPolygon, Polygon, Position } from 'geojson'
3
-
4
- export type ShapefileValidationResult =
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 validateShapefileGeometry(features: Feature[]): ShapefileValidationResult {
26
- if (features.length !== 1) {
27
- return { ok: false, error: 'The shapefile must contain exactly one geometry' }
28
- }
29
-
30
- const feature = features[0]
31
- const geometry = feature.geometry
32
-
33
- if (!geometry || (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon')) {
34
- return { ok: false, error: 'Geometry must be Polygon or MultiPolygon' }
35
- }
36
-
37
- if (!validateRingsClosed(geometry)) {
38
- return { ok: false, error: 'Polygon ring is not closed or is invalid' }
39
- }
40
-
41
- if (!booleanValid(geometry)) {
42
- return { ok: false, error: 'Invalid geometry' }
43
- }
44
-
45
- if (kinks(geometry).features.length > 0) {
46
- return { ok: false, error: 'Geometry has self-intersection' }
47
- }
48
-
49
- if (area(geometry) <= 0) {
50
- return { ok: false, error: 'Polygon has zero area' }
51
- }
52
-
53
- return { ok: true, feature: feature as Feature<Polygon | MultiPolygon> }
54
- }