Choosing Shapefile in the Export GIS modal produced no file. shp-write@0.3.2 cannot run in this build, in three separate ways: its dbf dependency assigns an undeclared variable, which was fine in the browserify bundle it was written for but is a ReferenceError under the strict mode ES modules always use; its zip.js branches on process.browser, which does not exist in a browser; and its download.js delivers the archive by navigating to a data:application/zip URL, which Chromium blocks in the top frame. - src/shapefile-write.js writes .shp/.shx/.dbf/.prj/.cpg itself: geometries split by type into up to three layers, polygon rings re-wound clockwise as the format requires, DBF column types inferred per column, names sanitised to DBF's 10 characters and de-duplicated, UTF-8 declared via .cpg. - src/zip-store.js holds the STORED-ZIP writer that was inside analysis/xlsx.js, so both writers share it. Pure-ASCII archives are byte-for-byte as before. - shp-write, and with it dbf and JSZip 2.5.0, leave the dependency tree; the build no longer warns that node's "assert" was externalised. - Service Worker v26 for the new bundle. Verified in a browser: the Export GIS modal, driven through its own controls with Web Mercator features, produced area_analysis.zip holding both layers with WGS84 coordinates, file code 9994 and the right shape types. The archive reads back through the application's own shpjs importer with geometries, numeric, text and boolean attributes intact. The zonal-statistics .xlsx export, which now shares the zip writer, still opens and reads correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
5.8 KiB
JavaScript
171 lines
5.8 KiB
JavaScript
/**
|
|
* 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<ol/Feature> in EPSG:3857 (map projection)
|
|
* rename, // Record<sourceField, exportFieldOrEmpty>
|
|
* format, // 'geojson' | 'shp' | 'kml'
|
|
* filenameBase, // 'area_analysis' (no extension)
|
|
* }) → Promise<void>
|
|
*
|
|
* 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 <name>
|
|
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`);
|
|
}
|