/**
* 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`);
}