1+ import fs from "fs/promises" ;
2+ import path from "path" ;
3+ import { DOMParser } from "xmldom" ;
4+ import * as turf from "@turf/turf" ;
5+
6+ // Minimal KML -> GeoJSON parser (supports Polygon/MultiPolygon/LineString/Point)
7+ // For production можно заменить на полноценный парсер, но этот уже рабочий для AOI-полигонов.
8+ function kmlToGeoJSON ( kmlText ) {
9+ const doc = new DOMParser ( ) . parseFromString ( kmlText , "text/xml" ) ;
10+ const placemarks = Array . from ( doc . getElementsByTagName ( "Placemark" ) ) ;
11+
12+ const features = placemarks . map ( ( pm ) => {
13+ const nameEl = pm . getElementsByTagName ( "name" ) [ 0 ] ;
14+ const name = nameEl ? nameEl . textContent : "AOI" ;
15+
16+ const poly = pm . getElementsByTagName ( "Polygon" ) [ 0 ] ;
17+ const line = pm . getElementsByTagName ( "LineString" ) [ 0 ] ;
18+ const point = pm . getElementsByTagName ( "Point" ) [ 0 ] ;
19+
20+ const parseCoords = ( coordsText ) =>
21+ coordsText
22+ . trim ( )
23+ . split ( / \s + / )
24+ . map ( ( tuple ) => tuple . split ( "," ) . map ( Number ) )
25+ . map ( ( [ lon , lat ] ) => [ lon , lat ] ) ;
26+
27+ if ( poly ) {
28+ const coordsEl = poly . getElementsByTagName ( "coordinates" ) [ 0 ] ;
29+ if ( ! coordsEl ) throw new Error ( "KML Polygon missing coordinates" ) ;
30+ const ring = parseCoords ( coordsEl . textContent ) ;
31+ // ensure closed ring
32+ const first = ring [ 0 ] ;
33+ const last = ring [ ring . length - 1 ] ;
34+ if ( first && last && ( first [ 0 ] !== last [ 0 ] || first [ 1 ] !== last [ 1 ] ) ) ring . push ( first ) ;
35+
36+ return {
37+ type : "Feature" ,
38+ properties : { name } ,
39+ geometry : { type : "Polygon" , coordinates : [ ring ] } ,
40+ } ;
41+ }
42+
43+ if ( line ) {
44+ const coordsEl = line . getElementsByTagName ( "coordinates" ) [ 0 ] ;
45+ if ( ! coordsEl ) throw new Error ( "KML LineString missing coordinates" ) ;
46+ const coords = parseCoords ( coordsEl . textContent ) ;
47+ return {
48+ type : "Feature" ,
49+ properties : { name } ,
50+ geometry : { type : "LineString" , coordinates : coords } ,
51+ } ;
52+ }
53+
54+ if ( point ) {
55+ const coordsEl = point . getElementsByTagName ( "coordinates" ) [ 0 ] ;
56+ if ( ! coordsEl ) throw new Error ( "KML Point missing coordinates" ) ;
57+ const [ lon , lat ] = coordsEl . textContent . trim ( ) . split ( "," ) . map ( Number ) ;
58+ return {
59+ type : "Feature" ,
60+ properties : { name } ,
61+ geometry : { type : "Point" , coordinates : [ lon , lat ] } ,
62+ } ;
63+ }
64+
65+ // If nothing matched:
66+ return null ;
67+ } ) . filter ( Boolean ) ;
68+
69+ return { type : "FeatureCollection" , features } ;
70+ }
71+
72+ function normalizeToFeatureCollection ( geojson ) {
73+ if ( ! geojson ) throw new Error ( "Empty AOI" ) ;
74+ if ( geojson . type === "FeatureCollection" ) return geojson ;
75+ if ( geojson . type === "Feature" ) return { type : "FeatureCollection" , features : [ geojson ] } ;
76+ // Geometry
77+ if ( geojson . type && geojson . coordinates ) return { type : "FeatureCollection" , features : [ { type : "Feature" , properties : { } , geometry : geojson } ] } ;
78+ throw new Error ( "Unsupported AOI GeoJSON structure" ) ;
79+ }
80+
81+ export async function parseAoiFileToFeatureCollection ( filePath , originalName = "" ) {
82+ const ext = ( path . extname ( originalName || filePath ) || "" ) . toLowerCase ( ) ;
83+
84+ const raw = await fs . readFile ( filePath , "utf-8" ) ;
85+
86+ if ( ext === ".geojson" || ext === ".json" ) {
87+ const parsed = JSON . parse ( raw ) ;
88+ return normalizeToFeatureCollection ( parsed ) ;
89+ }
90+
91+ if ( ext === ".kml" ) {
92+ return normalizeToFeatureCollection ( kmlToGeoJSON ( raw ) ) ;
93+ }
94+
95+ throw new Error ( `Unsupported AOI format: ${ ext } . Use .geojson/.json or .kml` ) ;
96+ }
97+
98+ export function computeAreaKm2 ( featureCollection ) {
99+ // Sum polygon areas; for non-polygons, area ~0
100+ let sum = 0 ;
101+ for ( const f of featureCollection . features || [ ] ) {
102+ try {
103+ const geom = f ?. geometry ;
104+ if ( ! geom ) continue ;
105+
106+ if ( geom . type === "Polygon" || geom . type === "MultiPolygon" ) {
107+ const areaM2 = turf . area ( f ) ;
108+ sum += areaM2 ;
109+ }
110+ } catch ( _ ) {
111+ // ignore malformed features
112+ }
113+ }
114+ const km2 = sum / 1_000_000 ;
115+ return Math . round ( km2 * 1000 ) / 1000 ; // 0.001 km² precision
116+ }
0 commit comments