/** * GIS export — write a list of OL features (with renamed properties) to * GeoJSON / KML / Shapefile, and trigger a browser download. * * Public API: * * exportFeaturesToGis({ * features, // Array
    in EPSG:3857 (map projection) * rename, // Record * format, // 'geojson' | 'shp' | 'kml' * filenameBase, // 'area_analysis' (no extension) * }) → Promise * * The output uses EPSG:4326 (lon/lat) in every format. Source fields whose * rename value is empty are dropped. Geometry is preserved as-is. * * SHP attribute names are truncated/sanitised to 10 chars + alphanumeric/ * underscore (DBF requirement). Collisions are resolved by appending a * numeric suffix. */ import GeoJSONFormat from 'ol/format/GeoJSON.js'; import KMLFormat from 'ol/format/KML.js'; import { buildShapefileZip } from './shapefile-write.js'; import { downloadBlob } from './zip-store.js'; const PROJ_MAP = 'EPSG:3857'; const PROJ_WGS84 = 'EPSG:4326'; // --------------------------------------------------------------------------- // Public entry // --------------------------------------------------------------------------- export async function exportFeaturesToGis({ features, rename, format, filenameBase = 'export', }) { if (!Array.isArray(features) || features.length === 0) { throw new Error('No features to export'); } // 1. Apply the rename map → produce GeoJSON Features in EPSG:4326. const geojson = buildRenamedGeoJSON(features, rename); // 2. Format-specific writer + download. switch (format) { case 'geojson': return downloadBlob( new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/geo+json' }), `${filenameBase}.geojson` ); case 'kml': return downloadBlob( new Blob([writeKml(features, rename)], { type: 'application/vnd.google-earth.kml+xml' }), `${filenameBase}.kml` ); case 'shp': return writeShp(geojson, filenameBase); default: throw new Error(`Unknown export format: ${format}`); } } // --------------------------------------------------------------------------- // Shared: apply rename → GeoJSON in WGS84 // --------------------------------------------------------------------------- /** * Produce a GeoJSON FeatureCollection in EPSG:4326 with properties remapped * per the user's rename table. Fields whose rename value is empty/null are * dropped. */ function buildRenamedGeoJSON(features, rename) { const fmt = new GeoJSONFormat(); const out = { type: 'FeatureCollection', features: [] }; for (const f of features) { if (!f.getGeometry()) continue; const geomGeoJson = fmt.writeGeometryObject(f.getGeometry(), { dataProjection: PROJ_WGS84, featureProjection: PROJ_MAP, }); const sourceProps = stripGeometryFromProps(f.getProperties()); const outProps = {}; for (const [src, target] of Object.entries(rename || {})) { if (!target) continue; // user cleared → drop if (Object.prototype.hasOwnProperty.call(sourceProps, src)) { outProps[target] = sourceProps[src]; } } out.features.push({ type: 'Feature', geometry: geomGeoJson, properties: outProps }); } return out; } function stripGeometryFromProps(props) { const out = {}; for (const [k, v] of Object.entries(props || {})) { if (k === 'geometry') continue; out[k] = v; } return out; } // --------------------------------------------------------------------------- // KML — uses OL's writer for proper styling/structure // --------------------------------------------------------------------------- function writeKml(features, rename) { // KML carries arbitrary string properties via ExtendedData; OL's writer // serialises every property. Clone each feature so we can swap its // properties to the renamed set without mutating the source. const clones = features .filter((f) => f.getGeometry()) .map((f) => { const clone = f.clone(); const sourceProps = stripGeometryFromProps(f.getProperties()); const outProps = {}; for (const [src, target] of Object.entries(rename || {})) { if (!target) continue; if (Object.prototype.hasOwnProperty.call(sourceProps, src)) { outProps[target] = sourceProps[src]; } } clone.setProperties(outProps, /* silent */ true); // Use the first renamed value that looks like a label for KML's const labelKey = Object.values(rename || {}).find( (v) => v && outProps[v] != null && outProps[v] !== '' ); if (labelKey) clone.set('name', String(outProps[labelKey])); return clone; }); return new KMLFormat({ extractStyles: false }).writeFeatures(clones, { dataProjection: PROJ_WGS84, featureProjection: PROJ_MAP, }); } // --------------------------------------------------------------------------- // Shapefile — shapefile-write.js builds a .zip of .shp/.shx/.dbf/.prj/.cpg // --------------------------------------------------------------------------- /** * A shapefile holds one geometry type, so the writer splits the collection * into up to three sets inside a single .zip. Attribute names are sanitised * to DBF's 10-character limit by the writer itself. * * This used to call shp-write's `download()`, which is unusable under Vite: * see the header of shapefile-write.js for what it did wrong. */ function writeShp(geojson, filenameBase) { const blob = buildShapefileZip(geojson, { folder: filenameBase, types: { point: `${filenameBase}_point`, polygon: `${filenameBase}_polygon`, polyline: `${filenameBase}_line`, }, }); downloadBlob(blob, `${filenameBase}.zip`); }