@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.
Files changed (71) hide show
  1. package/README.md +444 -0
  2. package/dist/constants/coordinatePanel.d.ts +1 -1
  3. package/dist/handlers/drawingControl.d.ts +0 -1
  4. package/dist/handlers/mapHandler.d.ts +8 -3
  5. package/dist/handlers/mapToolsHandler.d.ts +39 -0
  6. package/dist/handlers/measureToolsHandler.d.ts +51 -0
  7. package/dist/handlers/memorialButtonControl.d.ts +19 -0
  8. package/dist/handlers/toolsConstants.d.ts +3 -0
  9. package/dist/index.css +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.mjs +32914 -25131
  12. package/dist/types/index.d.ts +69 -0
  13. package/dist/utils/areaCalculator.d.ts +3 -0
  14. package/dist/utils/drawingPathOptions.d.ts +8 -0
  15. package/dist/utils/geometryCalculator.d.ts +13 -0
  16. package/dist/utils/geometryFileParseResult.d.ts +8 -0
  17. package/dist/utils/memorialLayer.d.ts +3 -0
  18. package/dist/utils/menuHistory.d.ts +8 -0
  19. package/dist/utils/normalizeGeoJsonToFeatures.d.ts +2 -0
  20. package/dist/utils/parseGeoJsonFile.d.ts +2 -0
  21. package/dist/utils/parseKmlFile.d.ts +2 -0
  22. package/dist/utils/parseShapefileZip.d.ts +3 -0
  23. package/dist/utils/stableMarker.d.ts +15 -0
  24. package/dist/utils/validateImportedPolygonGeometry.d.ts +9 -0
  25. package/package.json +22 -4
  26. package/src/assets/icons/file-import-regular-full-gray.svg +1 -0
  27. package/src/assets/icons/file-lines-regular-full-gray.svg +1 -0
  28. package/src/assets/icons/layer-icon.svg +3 -0
  29. package/src/assets/icons/loading.gif +0 -0
  30. package/src/assets/icons/marker-icon.svg +1 -0
  31. package/src/assets/icons/measure-icon.svg +5 -0
  32. package/src/assets/layers/mapLayers.json +16 -0
  33. package/src/assets/main.css +187 -0
  34. package/src/assets/styles/element-plus.css +25 -0
  35. package/src/assets/styles/layers-menu.css +95 -0
  36. package/src/assets/styles/leaflet.css +361 -0
  37. package/src/components/MapDPG.vue +302 -0
  38. package/src/components/coordinate/CoordinatePanel.vue +1206 -0
  39. package/src/components/fa-icon/FontAwesomeIcon.vue +44 -0
  40. package/src/components/loading/Loading.vue +25 -0
  41. package/src/components/map/LeafletMap.vue +183 -0
  42. package/src/components/menu/ChildMenu.vue +112 -0
  43. package/src/components/menu/LayerMenu.vue +294 -0
  44. package/src/components/menu/ParentMenu.vue +132 -0
  45. package/src/constants/coordinatePanel.ts +68 -0
  46. package/src/handlers/constants.ts +35 -0
  47. package/src/handlers/drawingControl.ts +143 -0
  48. package/src/handlers/mapHandler.ts +124 -0
  49. package/src/handlers/mapToolsHandler.ts +281 -0
  50. package/src/handlers/measureToolsHandler.ts +424 -0
  51. package/src/handlers/memorialButtonControl.ts +171 -0
  52. package/src/handlers/toolsConstants.ts +69 -0
  53. package/src/index.ts +6 -0
  54. package/src/shims-vue.d.ts +5 -0
  55. package/src/types/index.ts +260 -0
  56. package/src/types/json.d.ts +4 -0
  57. package/src/types/shpjs.d.ts +7 -0
  58. package/src/types/svg.d.ts +4 -0
  59. package/src/utils/CoordinateConverter.ts +43 -0
  60. package/src/utils/areaCalculator.ts +9 -0
  61. package/src/utils/drawingPathOptions.ts +57 -0
  62. package/src/utils/geometryCalculator.ts +62 -0
  63. package/src/utils/geometryFileParseResult.ts +5 -0
  64. package/src/utils/memorialLayer.ts +7 -0
  65. package/src/utils/menuHistory.ts +71 -0
  66. package/src/utils/normalizeGeoJsonToFeatures.ts +52 -0
  67. package/src/utils/parseGeoJsonFile.ts +23 -0
  68. package/src/utils/parseKmlFile.ts +31 -0
  69. package/src/utils/parseShapefileZip.ts +29 -0
  70. package/src/utils/stableMarker.ts +52 -0
  71. 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,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,7 @@
1
+ import type L from 'leaflet'
2
+
3
+ export const MEMORIAL_KEY = 'memorial'
4
+
5
+ export function isMemorialLayer(layer: L.Layer): boolean {
6
+ return (layer as L.Layer & { options?: { memorialKey?: string } }).options?.memorialKey === MEMORIAL_KEY
7
+ }
@@ -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
+ }