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>
113 lines
3.8 KiB
JavaScript
113 lines
3.8 KiB
JavaScript
/**
|
|
* zip-store.js — a minimal, dependency-free STORED (uncompressed) ZIP writer.
|
|
*
|
|
* Extracted from analysis/xlsx.js so the shapefile writer can share it. Both
|
|
* produce small archives (a few analysis rows, a handful of drawn features)
|
|
* where DEFLATE would save nothing worth a compression dependency, and every
|
|
* reader — Excel, QGIS, GDAL, unzip — accepts stored entries.
|
|
*
|
|
* zipStore({ 'a/b.txt': 'hello', 'a/c.bin': new Uint8Array([1, 2]) })
|
|
*
|
|
* Values may be strings (encoded UTF-8), Uint8Array, or ArrayBuffer.
|
|
*/
|
|
|
|
const CRC_TABLE = (() => {
|
|
const t = new Uint32Array(256);
|
|
for (let n = 0; n < 256; n++) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
t[n] = c >>> 0;
|
|
}
|
|
return t;
|
|
})();
|
|
|
|
function crc32(bytes) {
|
|
let c = 0xFFFFFFFF;
|
|
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8);
|
|
return (c ^ 0xFFFFFFFF) >>> 0;
|
|
}
|
|
|
|
function toBytes(enc, value) {
|
|
if (value instanceof Uint8Array) return value;
|
|
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
if (ArrayBuffer.isView(value)) {
|
|
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
}
|
|
return enc.encode(String(value));
|
|
}
|
|
|
|
/**
|
|
* Pack files (path → string | Uint8Array | ArrayBuffer) into an uncompressed
|
|
* ZIP Blob.
|
|
*
|
|
* @param {Record<string, string|Uint8Array|ArrayBuffer>} files
|
|
* @param {string} [mimeType] Blob type for the result.
|
|
* @returns {Blob}
|
|
*/
|
|
export function zipStore(files, mimeType = 'application/zip') {
|
|
const enc = new TextEncoder();
|
|
const entries = Object.entries(files).map(([name, value]) => {
|
|
const nameBytes = enc.encode(name);
|
|
return {
|
|
nameBytes,
|
|
data: toBytes(enc, value),
|
|
// Bit 11 declares the name is UTF-8. Only set when it matters, so
|
|
// pure-ASCII archives keep byte-for-byte the output xlsx.js had before.
|
|
flags: nameBytes.length === name.length ? 0 : 0x0800,
|
|
};
|
|
});
|
|
|
|
// DOS time/date — a fixed timestamp keeps output deterministic.
|
|
const time = 0, date = ((2020 - 1980) << 9) | (1 << 5) | 1;
|
|
|
|
const chunks = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
|
|
const u16 = (v) => [v & 0xFF, (v >>> 8) & 0xFF];
|
|
const u32 = (v) => [v & 0xFF, (v >>> 8) & 0xFF, (v >>> 16) & 0xFF, (v >>> 24) & 0xFF];
|
|
|
|
for (const e of entries) {
|
|
const crc = crc32(e.data);
|
|
const size = e.data.length;
|
|
const local = [
|
|
...u32(0x04034b50), ...u16(20), ...u16(e.flags), ...u16(0),
|
|
...u16(time), ...u16(date), ...u32(crc), ...u32(size), ...u32(size),
|
|
...u16(e.nameBytes.length), ...u16(0),
|
|
];
|
|
chunks.push(new Uint8Array(local), e.nameBytes, e.data);
|
|
|
|
central.push([
|
|
...u32(0x02014b50), ...u16(20), ...u16(20), ...u16(e.flags), ...u16(0),
|
|
...u16(time), ...u16(date), ...u32(crc), ...u32(size), ...u32(size),
|
|
...u16(e.nameBytes.length), ...u16(0), ...u16(0), ...u16(0), ...u16(0),
|
|
...u32(0), ...u32(offset),
|
|
]);
|
|
central.push(e.nameBytes);
|
|
offset += local.length + e.nameBytes.length + size;
|
|
}
|
|
|
|
const cdParts = [];
|
|
let cdSize = 0;
|
|
for (const part of central) {
|
|
const arr = part instanceof Uint8Array ? part : new Uint8Array(part);
|
|
cdParts.push(arr); cdSize += arr.length;
|
|
}
|
|
const end = new Uint8Array([
|
|
...u32(0x06054b50), ...u16(0), ...u16(0),
|
|
...u16(entries.length), ...u16(entries.length),
|
|
...u32(cdSize), ...u32(offset), ...u16(0),
|
|
]);
|
|
|
|
return new Blob([...chunks, ...cdParts, end], { type: mimeType });
|
|
}
|
|
|
|
/** Trigger a browser download of a Blob. */
|
|
export function downloadBlob(blob, filename) {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = filename;
|
|
document.body.appendChild(a); a.click(); a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
}
|