Shapefile export: write the format ourselves instead of shp-write
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>
This commit is contained in:
parent
d8b635b442
commit
8d1d874ad7
11
dist/assets/analysis-modal-CXnHlVxO.js
vendored
11
dist/assets/analysis-modal-CXnHlVxO.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/analysis-modal-CXnHlVxO.js.map
vendored
1
dist/assets/analysis-modal-CXnHlVxO.js.map
vendored
File diff suppressed because one or more lines are too long
12
dist/assets/analysis-modal-Ceula2pX.js
vendored
Normal file
12
dist/assets/analysis-modal-Ceula2pX.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/analysis-modal-Ceula2pX.js.map
vendored
Normal file
1
dist/assets/analysis-modal-Ceula2pX.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
933
dist/assets/index-BzlrAJOS.js
vendored
933
dist/assets/index-BzlrAJOS.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-BzlrAJOS.js.map
vendored
1
dist/assets/index-BzlrAJOS.js.map
vendored
File diff suppressed because one or more lines are too long
933
dist/assets/index-uzWunVfg.js
vendored
Normal file
933
dist/assets/index-uzWunVfg.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-uzWunVfg.js.map
vendored
Normal file
1
dist/assets/index-uzWunVfg.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
dist/assets/pako-Xa-UToif.js
vendored
2
dist/assets/pako-Xa-UToif.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/pako-Xa-UToif.js.map
vendored
1
dist/assets/pako-Xa-UToif.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/assets/pako-n3Pgozwg.js
vendored
Normal file
2
dist/assets/pako-n3Pgozwg.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/pako-n3Pgozwg.js.map
vendored
Normal file
1
dist/assets/pako-n3Pgozwg.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
import{E as f,a as b}from"./jspdf-BsOD2LDR.js";import"./pako-Xa-UToif.js";import"./geotiff-B9aROX49.js";import"./openlayers-D5-VSKzq.js";b(f);let s=null;async function x(){if(s)return s;try{const e=new Image;e.crossOrigin="anonymous",await new Promise((r,i)=>{e.onload=r,e.onerror=i,e.src="./app-icons/luspa-pdf.jpg"});const n=document.createElement("canvas");n.width=e.naturalWidth,n.height=e.naturalHeight;const t=n.getContext("2d");return t.fillStyle="#ffffff",t.fillRect(0,0,n.width,n.height),t.drawImage(e,0,0),s=n.toDataURL("image/jpeg",.92),s}catch(e){return console.warn("[PDF] Could not load logo:",e),null}}async function F({title:e,rows:n}){const t=new f({orientation:"portrait",unit:"mm",format:"a4"}),r=t.internal.pageSize.getWidth(),i=[30,26,75],g=await x(),c=28,a=14;let o=14;g&&t.addImage(g,"JPEG",a,o,c,c);const m=a+c+6;t.setFont("helvetica","bold"),t.setFontSize(18),t.setTextColor(...i),t.text("LUPMIS",m,o+11),t.setFont("helvetica","normal"),t.setFontSize(12),t.text(e,m,o+19);const d=new Date,h=d.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"}),p=d.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});t.setFontSize(9),t.setTextColor(120,120,120),t.text(`${h} ${p}`,r-a,o+11,{align:"right"}),o+=c+6,t.setDrawColor(...i),t.setLineWidth(.5),t.line(a,o,r-a,o),o+=6;const y=n.map(l=>[l.label,l.value]);t.autoTable({startY:o,head:[["Property","Value"]],body:y,margin:{left:a,right:a},styles:{font:"helvetica",fontSize:10,cellPadding:4},headStyles:{fillColor:i,textColor:[255,255,255],fontStyle:"bold"},alternateRowStyles:{fillColor:[245,245,250]},columnStyles:{0:{fontStyle:"bold",cellWidth:50}}});const S=t.lastAutoTable.finalY+10;t.setFontSize(8),t.setTextColor(160,160,160),t.text("Generated by LUPMIS2 Land Use Planning & Management Information System",a,S);const w=t.output("blob"),u=URL.createObjectURL(w);if(!window.open(u,"_blank")){const l=document.createElement("a");l.href=u,l.download=`${e.replace(/\s+/g,"_")}_${d.toISOString().slice(0,10)}.pdf`,document.body.appendChild(l),l.click(),document.body.removeChild(l)}}export{F as exportAnalysisPDF};
|
||||
//# sourceMappingURL=pdf-export-C0eAmANf.js.map
|
||||
import{E as f,a as b}from"./jspdf-Dh9Z6DYg.js";import"./pako-n3Pgozwg.js";import"./geotiff-B7h926ms.js";import"./openlayers-D5-VSKzq.js";b(f);let s=null;async function x(){if(s)return s;try{const e=new Image;e.crossOrigin="anonymous",await new Promise((r,i)=>{e.onload=r,e.onerror=i,e.src="./app-icons/luspa-pdf.jpg"});const n=document.createElement("canvas");n.width=e.naturalWidth,n.height=e.naturalHeight;const t=n.getContext("2d");return t.fillStyle="#ffffff",t.fillRect(0,0,n.width,n.height),t.drawImage(e,0,0),s=n.toDataURL("image/jpeg",.92),s}catch(e){return console.warn("[PDF] Could not load logo:",e),null}}async function F({title:e,rows:n}){const t=new f({orientation:"portrait",unit:"mm",format:"a4"}),r=t.internal.pageSize.getWidth(),i=[30,26,75],g=await x(),c=28,a=14;let o=14;g&&t.addImage(g,"JPEG",a,o,c,c);const m=a+c+6;t.setFont("helvetica","bold"),t.setFontSize(18),t.setTextColor(...i),t.text("LUPMIS",m,o+11),t.setFont("helvetica","normal"),t.setFontSize(12),t.text(e,m,o+19);const d=new Date,h=d.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"}),p=d.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});t.setFontSize(9),t.setTextColor(120,120,120),t.text(`${h} ${p}`,r-a,o+11,{align:"right"}),o+=c+6,t.setDrawColor(...i),t.setLineWidth(.5),t.line(a,o,r-a,o),o+=6;const y=n.map(l=>[l.label,l.value]);t.autoTable({startY:o,head:[["Property","Value"]],body:y,margin:{left:a,right:a},styles:{font:"helvetica",fontSize:10,cellPadding:4},headStyles:{fillColor:i,textColor:[255,255,255],fontStyle:"bold"},alternateRowStyles:{fillColor:[245,245,250]},columnStyles:{0:{fontStyle:"bold",cellWidth:50}}});const S=t.lastAutoTable.finalY+10;t.setFontSize(8),t.setTextColor(160,160,160),t.text("Generated by LUPMIS2 Land Use Planning & Management Information System",a,S);const w=t.output("blob"),u=URL.createObjectURL(w);if(!window.open(u,"_blank")){const l=document.createElement("a");l.href=u,l.download=`${e.replace(/\s+/g,"_")}_${d.toISOString().slice(0,10)}.pdf`,document.body.appendChild(l),l.click(),document.body.removeChild(l)}}export{F as exportAnalysisPDF};
|
||||
//# sourceMappingURL=pdf-export-cmdWg-4I.js.map
|
||||
File diff suppressed because one or more lines are too long
5
dist/assets/shpjs-CNrRgkgn.js
vendored
Normal file
5
dist/assets/shpjs-CNrRgkgn.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/shpjs-CNrRgkgn.js.map
vendored
Normal file
1
dist/assets/shpjs-CNrRgkgn.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
dist/assets/shpjs-Doisr2ug.js
vendored
6
dist/assets/shpjs-Doisr2ug.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/shpjs-Doisr2ug.js.map
vendored
1
dist/assets/shpjs-Doisr2ug.js.map
vendored
File diff suppressed because one or more lines are too long
30
dist/assets/xlsx-D1ZOqanH.js
vendored
Normal file
30
dist/assets/xlsx-D1ZOqanH.js
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import{d as f,z as d}from"./index-uzWunVfg.js";import"./geotiff-B7h926ms.js";import"./openlayers-D5-VSKzq.js";import"./pako-n3Pgozwg.js";import"./bootstrap-D1-uvFxm.js";import"./ol-ext-DV70ljNY.js";function p(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function h(e){let l="",o=e;for(;o>=0;)l=String.fromCharCode(o%26+65)+l,o=Math.floor(o/26)-1;return l}function x(e){return e&&!Array.isArray(e)&&Array.isArray(e.cells)?{cells:e.cells,bold:!!e.bold}:{cells:Array.isArray(e)?e:[e],bold:!1}}function u(e){const l=[];return e.forEach((o,n)=>{const{cells:t,bold:a}=x(o),r=a?' s="1"':"",i=t.map((s,c)=>{const m=`${h(c)}${n+1}`;return s==null||s===""?`<c r="${m}"${r}/>`:typeof s=="number"&&Number.isFinite(s)?`<c r="${m}"${r}><v>${s}</v></c>`:`<c r="${m}"${r} t="inlineStr"><is><t xml:space="preserve">${p(s)}</t></is></c>`}).join("");l.push(`<row r="${n+1}">${i}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${l.join("")}</sheetData></worksheet>`}const y=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>
|
||||
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
|
||||
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
|
||||
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
||||
<cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs>
|
||||
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
|
||||
</styleSheet>`;function g(e){const l=(e||[]).filter(t=>t&&Array.isArray(t.rows));if(l.length===0)throw new Error("No sheets to export.");const o={},n=l.map((t,a)=>{const r=`xl/worksheets/sheet${a+1}.xml`;o[r]=u(t.rows);const i=String(t.name||`Sheet${a+1}`).replace(/[:\\/?*[\]]/g," ").slice(0,31);return{id:a+1,name:i,file:r}});return o["[Content_Types].xml"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||||
${n.map(t=>`<Override PartName="/${t.file}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join(`
|
||||
`)}
|
||||
</Types>`,o["_rels/.rels"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
</Relationships>`,o["xl/workbook.xml"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>${n.map(t=>`<sheet name="${p(t.name)}" sheetId="${t.id}" r:id="rId${t.id}"/>`).join("")}</sheets>
|
||||
</workbook>`,o["xl/_rels/workbook.xml.rels"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
${n.map(t=>`<Relationship Id="rId${t.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${t.id}.xml"/>`).join(`
|
||||
`)}
|
||||
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
</Relationships>`,o["xl/styles.xml"]=y,d(o,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}function v(e,l){f(g(e),l)}export{g as buildXlsxBlob,v as exportXlsx};
|
||||
//# sourceMappingURL=xlsx-D1ZOqanH.js.map
|
||||
1
dist/assets/xlsx-D1ZOqanH.js.map
vendored
Normal file
1
dist/assets/xlsx-D1ZOqanH.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
30
dist/assets/xlsx-d1LrY7GM.js
vendored
30
dist/assets/xlsx-d1LrY7GM.js
vendored
@ -1,30 +0,0 @@
|
||||
function u(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function g(n){let o="",e=n;for(;e>=0;)o=String.fromCharCode(e%26+65)+o,e=Math.floor(e/26)-1;return o}function F(n){return n&&!Array.isArray(n)&&Array.isArray(n.cells)?{cells:n.cells,bold:!!n.bold}:{cells:Array.isArray(n)?n:[n],bold:!1}}function b(n){const o=[];return n.forEach((e,r)=>{const{cells:l,bold:c}=F(e),i=c?' s="1"':"",m=l.map((t,a)=>{const f=`${g(a)}${r+1}`;return t==null||t===""?`<c r="${f}"${i}/>`:typeof t=="number"&&Number.isFinite(t)?`<c r="${f}"${i}><v>${t}</v></c>`:`<c r="${f}"${i} t="inlineStr"><is><t xml:space="preserve">${u(t)}</t></is></c>`}).join("");o.push(`<row r="${r+1}">${m}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${o.join("")}</sheetData></worksheet>`}const k=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>
|
||||
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
|
||||
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
|
||||
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
||||
<cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs>
|
||||
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
|
||||
</styleSheet>`,T=(()=>{const n=new Uint32Array(256);for(let o=0;o<256;o++){let e=o;for(let r=0;r<8;r++)e=e&1?3988292384^e>>>1:e>>>1;n[o]=e>>>0}return n})();function I(n){let o=4294967295;for(let e=0;e<n.length;e++)o=T[(o^n[e])&255]^o>>>8;return(o^4294967295)>>>0}function $(n){const o=new TextEncoder,e=Object.entries(n).map(([s,p])=>({name:s,nameBytes:o.encode(s),data:o.encode(p)})),r=0,l=20513,c=[],i=[];let m=0;const t=s=>[s&255,s>>>8&255],a=s=>[s&255,s>>>8&255,s>>>16&255,s>>>24&255];for(const s of e){const p=I(s.data),d=s.data.length,x=[...a(67324752),...t(20),...t(0),...t(0),...t(r),...t(l),...a(p),...a(d),...a(d),...t(s.nameBytes.length),...t(0)];c.push(new Uint8Array(x),s.nameBytes,s.data),i.push([...a(33639248),...t(20),...t(20),...t(0),...t(0),...t(r),...t(l),...a(p),...a(d),...a(d),...t(s.nameBytes.length),...t(0),...t(0),...t(0),...t(0),...a(0),...a(m)]),i.push(s.nameBytes),m+=x.length+s.nameBytes.length+d}const f=[];let h=0;for(const s of i){const p=s instanceof Uint8Array?s:new Uint8Array(s);f.push(p),h+=p.length}const y=new Uint8Array([...a(101010256),...t(0),...t(0),...t(e.length),...t(e.length),...a(h),...a(m),...t(0)]);return new Blob([...c,...f,y],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"})}function w(n){const o=(n||[]).filter(l=>l&&Array.isArray(l.rows));if(o.length===0)throw new Error("No sheets to export.");const e={},r=o.map((l,c)=>{const i=`xl/worksheets/sheet${c+1}.xml`;e[i]=b(l.rows);const m=String(l.name||`Sheet${c+1}`).replace(/[:\\/?*[\]]/g," ").slice(0,31);return{id:c+1,name:m,file:i}});return e["[Content_Types].xml"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||||
${r.map(l=>`<Override PartName="/${l.file}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join(`
|
||||
`)}
|
||||
</Types>`,e["_rels/.rels"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
</Relationships>`,e["xl/workbook.xml"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>${r.map(l=>`<sheet name="${u(l.name)}" sheetId="${l.id}" r:id="rId${l.id}"/>`).join("")}</sheets>
|
||||
</workbook>`,e["xl/_rels/workbook.xml.rels"]=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
${r.map(l=>`<Relationship Id="rId${l.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${l.id}.xml"/>`).join(`
|
||||
`)}
|
||||
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
</Relationships>`,e["xl/styles.xml"]=k,$(e)}function S(n,o){const e=URL.createObjectURL(n),r=document.createElement("a");r.href=e,r.download=o,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(e),1e3)}function U(n,o){S(w(n),o)}export{w as buildXlsxBlob,S as downloadBlob,U as exportXlsx};
|
||||
//# sourceMappingURL=xlsx-d1LrY7GM.js.map
|
||||
1
dist/assets/xlsx-d1LrY7GM.js.map
vendored
1
dist/assets/xlsx-d1LrY7GM.js.map
vendored
File diff suppressed because one or more lines are too long
7
dist/index.html
vendored
7
dist/index.html
vendored
@ -1997,13 +1997,12 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-BzlrAJOS.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-uzWunVfg.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pako-Xa-UToif.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/geotiff-B9aROX49.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pako-n3Pgozwg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/geotiff-B7h926ms.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/bootstrap-D1-uvFxm.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ol-ext-DV70ljNY.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/shpjs-Doisr2ug.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/openlayers-BtPuoxOl.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/bootstrap-BtmJYOxZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/ol-ext-BgKrOIxx.css">
|
||||
|
||||
11
dist/sw.js
vendored
11
dist/sw.js
vendored
@ -143,7 +143,16 @@
|
||||
// threw away every tile officers had downloaded for field work. Tile caches
|
||||
// now carry their own TILE_CACHE_VERSION, frozen at v24, and tiles in any
|
||||
// older tile cache are moved into the current ones instead of deleted.
|
||||
const CACHE_VERSION = 'v25';
|
||||
// v26: Shapefile export works again. Choosing "Shapefile" in the Export GIS
|
||||
// modal threw "offset is not defined" and produced no file: shp-write's
|
||||
// dbf dependency assigns an undeclared variable, which is fine in the
|
||||
// browserify bundle it was written for but a ReferenceError under ESM's
|
||||
// strict mode. Two further breakages sat behind it — shp-write branches
|
||||
// on `process.browser`, and delivers the zip by navigating to a data:
|
||||
// URL, which Chromium blocks. src/shapefile-write.js now writes the
|
||||
// .shp/.shx/.dbf/.prj/.cpg itself and hands over a Blob, so shp-write,
|
||||
// dbf and JSZip are gone from the dependency tree. New hashed bundle.
|
||||
const CACHE_VERSION = 'v26';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -23,7 +23,6 @@
|
||||
"jspdf-autotable": "^5.0.7",
|
||||
"ol": "^10.3.0",
|
||||
"ol-ext": "^4.0.24",
|
||||
"shp-write": "^0.3.2",
|
||||
"shpjs": "^6.2.0",
|
||||
"sqlocal": "^0.16.0"
|
||||
},
|
||||
@ -1176,15 +1175,6 @@
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dbf": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dbf/-/dbf-0.1.4.tgz",
|
||||
"integrity": "sha512-7tQ8w5NB74PL1f0Z/NQ6Y+URjBFhtEsFxzEQSzot2+VpLwWfrNnxFVhzWm6dJyEtFq0WkYWcGEMDf39fy8JFaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"jdataview": "~2.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
|
||||
@ -1337,11 +1327,6 @@
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jdataview": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz",
|
||||
"integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw=="
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
|
||||
@ -1368,21 +1353,6 @@
|
||||
"jspdf": "^2 || ^3 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz",
|
||||
"integrity": "sha512-IRoyf8JSYY3nx+uyh5xPc0qdy8pUDTp2UkHOWYNF/IO/3D8nx7899UlSAjD8rf8wUgOmm0lACWx/GbW3EaxIXQ==",
|
||||
"license": "MIT or GPLv3",
|
||||
"dependencies": {
|
||||
"pako": "~0.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip/node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lerc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
|
||||
@ -1684,17 +1654,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/shp-write": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/shp-write/-/shp-write-0.3.2.tgz",
|
||||
"integrity": "sha512-RNmfm+qzIwgwGMiV21lCxfEAtgP/owAd+sHLr6Qu+aDR1bbrCZ42H89nA9FQWUqfL+WHJy3n8+cTZxJrL/ZKWA==",
|
||||
"deprecated": "This package has moved under the @mapbox organization, and can be found here: https://www.npmjs.com/package/@mapbox/shp-write",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dbf": "0.1.4",
|
||||
"jszip": "2.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shpjs": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.2.0.tgz",
|
||||
|
||||
@ -24,7 +24,6 @@
|
||||
"jspdf-autotable": "^5.0.7",
|
||||
"ol": "^10.3.0",
|
||||
"ol-ext": "^4.0.24",
|
||||
"shp-write": "^0.3.2",
|
||||
"shpjs": "^6.2.0",
|
||||
"sqlocal": "^0.16.0"
|
||||
},
|
||||
|
||||
11
public/sw.js
11
public/sw.js
@ -143,7 +143,16 @@
|
||||
// threw away every tile officers had downloaded for field work. Tile caches
|
||||
// now carry their own TILE_CACHE_VERSION, frozen at v24, and tiles in any
|
||||
// older tile cache are moved into the current ones instead of deleted.
|
||||
const CACHE_VERSION = 'v25';
|
||||
// v26: Shapefile export works again. Choosing "Shapefile" in the Export GIS
|
||||
// modal threw "offset is not defined" and produced no file: shp-write's
|
||||
// dbf dependency assigns an undeclared variable, which is fine in the
|
||||
// browserify bundle it was written for but a ReferenceError under ESM's
|
||||
// strict mode. Two further breakages sat behind it — shp-write branches
|
||||
// on `process.browser`, and delivers the zip by navigating to a data:
|
||||
// URL, which Chromium blocks. src/shapefile-write.js now writes the
|
||||
// .shp/.shx/.dbf/.prj/.cpg itself and hands over a Blob, so shp-write,
|
||||
// dbf and JSZip are gone from the dependency tree. New hashed bundle.
|
||||
const CACHE_VERSION = 'v26';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
|
||||
@ -2,14 +2,15 @@
|
||||
* xlsx.js — a minimal, dependency-free XLSX writer.
|
||||
*
|
||||
* An .xlsx file is a ZIP archive of XML parts. This module builds the few
|
||||
* parts Excel actually requires and packs them with a small STORED (uncompressed)
|
||||
* ZIP writer — readers accept stored entries, and an analysis table is far too
|
||||
* small for compression to matter.
|
||||
* parts Excel actually requires and packs them with ../zip-store.js, a small
|
||||
* STORED (uncompressed) ZIP writer — readers accept stored entries, and an
|
||||
* analysis table is far too small for compression to matter.
|
||||
*
|
||||
* Why not a library: SheetJS's npm package is stale and carries advisories,
|
||||
* ExcelJS is heavy for an offline-first field app, and the JSZip in node_modules
|
||||
* is only a transitive dependency of shp-write (so it could disappear). This is
|
||||
* ~200 lines we control, adds nothing to the dependency tree, and works offline.
|
||||
* Why not a library: SheetJS's npm package is stale and carries advisories, and
|
||||
* ExcelJS is heavy for an offline-first field app. This is ~200 lines we
|
||||
* control, adds nothing to the dependency tree, and works offline. (The JSZip
|
||||
* that used to sit in node_modules is gone: it came in only via shp-write,
|
||||
* which ../shapefile-write.js replaced.)
|
||||
*
|
||||
* buildXlsxBlob([{ name: 'Results', rows: [['Zone','Count'], ['North', 3]] }])
|
||||
*
|
||||
@ -18,6 +19,8 @@
|
||||
* as { cells: [...], bold: true } renders that row in bold.
|
||||
*/
|
||||
|
||||
import { zipStore, downloadBlob } from '../zip-store.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// XML helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -78,79 +81,6 @@ const STYLES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
|
||||
</styleSheet>`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minimal STORED-ZIP writer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** Pack files (name → string) into an uncompressed ZIP Blob. */
|
||||
function zipStore(files) {
|
||||
const enc = new TextEncoder();
|
||||
const entries = Object.entries(files).map(([name, text]) => ({
|
||||
name, nameBytes: enc.encode(name), data: enc.encode(text),
|
||||
}));
|
||||
|
||||
// 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(0), ...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(0), ...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: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -204,16 +134,8 @@ ${sheetEntries.map((s) => `<Relationship Id="rId${s.id}" Type="http://schemas.op
|
||||
|
||||
files['xl/styles.xml'] = STYLES_XML;
|
||||
|
||||
return zipStore(files);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
return zipStore(files,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
}
|
||||
|
||||
/** Convenience: build and download in one call. */
|
||||
|
||||
@ -21,7 +21,8 @@
|
||||
|
||||
import GeoJSONFormat from 'ol/format/GeoJSON.js';
|
||||
import KMLFormat from 'ol/format/KML.js';
|
||||
import * as shpwrite from 'shp-write';
|
||||
import { buildShapefileZip } from './shapefile-write.js';
|
||||
import { downloadBlob } from './zip-store.js';
|
||||
|
||||
const PROJ_MAP = 'EPSG:3857';
|
||||
const PROJ_WGS84 = 'EPSG:4326';
|
||||
@ -145,89 +146,25 @@ function writeKml(features, rename) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shapefile — uses shp-write to build a .zip with .shp/.shx/.dbf/.prj
|
||||
// Shapefile — shapefile-write.js builds a .zip of .shp/.shx/.dbf/.prj/.cpg
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* shp-write's `download` accepts a GeoJSON FeatureCollection and a config.
|
||||
* It splits geometries by type and writes one shapefile per type inside a
|
||||
* single .zip download. We pre-sanitise field names to honour DBF's 10-char
|
||||
* limit (otherwise shp-write silently truncates, possibly causing collisions).
|
||||
* 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.
|
||||
*/
|
||||
async function writeShp(geojson, filenameBase) {
|
||||
const sanitised = sanitiseShpFieldNames(geojson);
|
||||
|
||||
// shp-write 0.3 emits a .zip via DOM download. There's no Promise return,
|
||||
// but the call is synchronous in practice. Wrap to a Promise so callers
|
||||
// can await uniformly.
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
shpwrite.download(sanitised, {
|
||||
folder: filenameBase,
|
||||
outputType: 'blob',
|
||||
compression: 'DEFLATE',
|
||||
types: {
|
||||
point: `${filenameBase}_point`,
|
||||
polygon: `${filenameBase}_polygon`,
|
||||
polyline: `${filenameBase}_line`,
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
function writeShp(geojson, filenameBase) {
|
||||
const blob = buildShapefileZip(geojson, {
|
||||
folder: filenameBase,
|
||||
types: {
|
||||
point: `${filenameBase}_point`,
|
||||
polygon: `${filenameBase}_polygon`,
|
||||
polyline: `${filenameBase}_line`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make every property key valid for DBF: ≤10 chars, alphanumeric/underscore,
|
||||
* unique across the set. Returns a new GeoJSON FeatureCollection.
|
||||
*/
|
||||
function sanitiseShpFieldNames(geojson) {
|
||||
// Collect the union of source keys (sources are already user-renamed).
|
||||
const allKeys = new Set();
|
||||
for (const f of geojson.features) {
|
||||
for (const k of Object.keys(f.properties || {})) allKeys.add(k);
|
||||
}
|
||||
|
||||
// Build map sourceKey → safeKey.
|
||||
const used = new Set();
|
||||
const remap = {};
|
||||
for (const k of allKeys) {
|
||||
let base = String(k).replace(/[^A-Za-z0-9_]+/g, '_').slice(0, 10) || 'field';
|
||||
let candidate = base;
|
||||
let i = 1;
|
||||
while (used.has(candidate)) {
|
||||
const suffix = String(i++);
|
||||
candidate = base.slice(0, Math.max(1, 10 - suffix.length)) + suffix;
|
||||
}
|
||||
used.add(candidate);
|
||||
remap[k] = candidate;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: geojson.features.map((f) => {
|
||||
const newProps = {};
|
||||
for (const [k, v] of Object.entries(f.properties || {})) {
|
||||
newProps[remap[k]] = v;
|
||||
}
|
||||
return { type: 'Feature', geometry: f.geometry, properties: newProps };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic download helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
downloadBlob(blob, `${filenameBase}.zip`);
|
||||
}
|
||||
|
||||
529
src/shapefile-write.js
Normal file
529
src/shapefile-write.js
Normal file
@ -0,0 +1,529 @@
|
||||
/**
|
||||
* shapefile-write.js — write ESRI Shapefiles from a GeoJSON FeatureCollection,
|
||||
* with no dependencies.
|
||||
*
|
||||
* Replaces shp-write@0.3.2, which cannot run in a Vite/ESM build:
|
||||
* • its `dbf` dependency assigns an undeclared `offset` (fine in the sloppy-
|
||||
* mode browserify bundle it was written for, a ReferenceError under ESM's
|
||||
* always-on strict mode);
|
||||
* • src/zip.js branches on `process.browser`, which does not exist in the
|
||||
* browser;
|
||||
* • src/download.js navigates to a `data:application/zip` URL, which Chromium
|
||||
* blocks in the top frame;
|
||||
* • src/write.js imports node's `assert`, which Vite externalises with a
|
||||
* build warning.
|
||||
* It also pulled in JSZip 2.5.0 (2014). Writing the three binary parts here is
|
||||
* ~300 lines we control, and matches analysis/xlsx.js, which already builds its
|
||||
* own container rather than depending on that JSZip.
|
||||
*
|
||||
* buildShapefileZip(featureCollection, { folder, types }) → Blob (.zip)
|
||||
*
|
||||
* Geometries are split by type into up to three layers (point / polyline /
|
||||
* polygon), because a shapefile holds exactly one geometry type. Coordinates
|
||||
* are written as-is, so callers must pass EPSG:4326 — the .prj says WGS84.
|
||||
*
|
||||
* Format reference: ESRI Shapefile Technical Description (July 1998), and
|
||||
* dBASE III for the .dbf.
|
||||
*/
|
||||
|
||||
import { zipStore } from './zip-store.js';
|
||||
|
||||
const SHP_POINT = 1;
|
||||
const SHP_POLYLINE = 3;
|
||||
const SHP_POLYGON = 5;
|
||||
|
||||
// Same WKT shp-write emitted; GDAL and QGIS both read it as EPSG:4326.
|
||||
const PRJ_WGS84 =
|
||||
'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],' +
|
||||
'PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public entry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a .zip Blob holding one shapefile set (.shp/.shx/.dbf/.prj/.cpg) per
|
||||
* geometry type present in the collection.
|
||||
*
|
||||
* @param {Object} geojson FeatureCollection, coordinates in EPSG:4326.
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.folder] Folder inside the zip; omit for the root.
|
||||
* @param {{point?: string, polyline?: string, polygon?: string}} [options.types]
|
||||
* Base filename per layer.
|
||||
* @returns {Blob}
|
||||
*/
|
||||
export function buildShapefileZip(geojson, options = {}) {
|
||||
const layers = splitByGeometryType(geojson);
|
||||
const present = Object.keys(layers).filter((k) => layers[k].length > 0);
|
||||
if (present.length === 0) {
|
||||
throw new Error('No point, line or polygon geometries to export.');
|
||||
}
|
||||
|
||||
const dir = options.folder ? `${String(options.folder).replace(/\/+$/, '')}/` : '';
|
||||
const files = {};
|
||||
|
||||
for (const kind of present) {
|
||||
const records = layers[kind];
|
||||
const base = (options.types && options.types[kind]) || kind;
|
||||
const { shp, shx } = buildShpShx(records, SHAPE_TYPE[kind]);
|
||||
|
||||
files[`${dir}${base}.shp`] = shp;
|
||||
files[`${dir}${base}.shx`] = shx;
|
||||
files[`${dir}${base}.dbf`] = buildDbf(records.map((r) => r.properties));
|
||||
files[`${dir}${base}.prj`] = PRJ_WGS84;
|
||||
// Tells GDAL/QGIS the .dbf character fields are UTF-8 rather than the
|
||||
// dBASE default codepage, so non-ASCII attribute values survive.
|
||||
files[`${dir}${base}.cpg`] = 'UTF-8';
|
||||
}
|
||||
|
||||
return zipStore(files, 'application/zip');
|
||||
}
|
||||
|
||||
const SHAPE_TYPE = {
|
||||
point: SHP_POINT,
|
||||
polyline: SHP_POLYLINE,
|
||||
polygon: SHP_POLYGON,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GeoJSON → per-layer records
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sort features into point / polyline / polygon buckets.
|
||||
*
|
||||
* Each record is `{ parts, properties }`, where `parts` is an array of
|
||||
* coordinate rings (one for a point layer, one per line/ring otherwise).
|
||||
* Multi-geometries collapse into a single multi-part record, except
|
||||
* MultiPoint, which shapefiles cannot store in a Point layer — those expand
|
||||
* into one record per position, repeating the attributes.
|
||||
*/
|
||||
function splitByGeometryType(geojson) {
|
||||
const out = { point: [], polyline: [], polygon: [] };
|
||||
const features = (geojson && geojson.features) || [];
|
||||
|
||||
for (const f of features) {
|
||||
const props = (f && f.properties) || {};
|
||||
for (const g of flattenGeometry(f && f.geometry)) {
|
||||
switch (g.type) {
|
||||
case 'Point':
|
||||
if (isXY(g.coordinates)) out.point.push({ parts: [[g.coordinates]], properties: props });
|
||||
break;
|
||||
|
||||
case 'MultiPoint':
|
||||
for (const pos of g.coordinates || []) {
|
||||
if (isXY(pos)) out.point.push({ parts: [[pos]], properties: props });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LineString': {
|
||||
const parts = [cleanRing(g.coordinates)].filter((p) => p.length >= 2);
|
||||
if (parts.length) out.polyline.push({ parts, properties: props });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MultiLineString': {
|
||||
const parts = (g.coordinates || []).map(cleanRing).filter((p) => p.length >= 2);
|
||||
if (parts.length) out.polyline.push({ parts, properties: props });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Polygon': {
|
||||
const parts = polygonParts(g.coordinates);
|
||||
if (parts.length) out.polygon.push({ parts, properties: props });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MultiPolygon': {
|
||||
const parts = [];
|
||||
for (const poly of g.coordinates || []) parts.push(...polygonParts(poly));
|
||||
if (parts.length) out.polygon.push({ parts, properties: props });
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Expand GeometryCollection; yield plain geometries. */
|
||||
function* flattenGeometry(geom) {
|
||||
if (!geom || !geom.type) return;
|
||||
if (geom.type === 'GeometryCollection') {
|
||||
for (const g of geom.geometries || []) yield* flattenGeometry(g);
|
||||
} else {
|
||||
yield geom;
|
||||
}
|
||||
}
|
||||
|
||||
function isXY(pos) {
|
||||
return Array.isArray(pos) && Number.isFinite(pos[0]) && Number.isFinite(pos[1]);
|
||||
}
|
||||
|
||||
/** Drop malformed positions and consecutive duplicates. */
|
||||
function cleanRing(coords) {
|
||||
const out = [];
|
||||
for (const pos of coords || []) {
|
||||
if (!isXY(pos)) continue;
|
||||
const last = out[out.length - 1];
|
||||
if (last && last[0] === pos[0] && last[1] === pos[1]) continue;
|
||||
out.push([pos[0], pos[1]]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare one GeoJSON polygon's rings for the shapefile.
|
||||
*
|
||||
* Shapefiles orient the opposite way to GeoJSON (RFC 7946): outer rings run
|
||||
* clockwise and holes counter-clockwise. GeoJSON producers are inconsistent
|
||||
* about this, so rather than trusting the input we measure each ring's signed
|
||||
* area and flip it when needed. Rings are also explicitly closed.
|
||||
*/
|
||||
function polygonParts(rings) {
|
||||
const out = [];
|
||||
(rings || []).forEach((raw, i) => {
|
||||
const ring = closeRing(cleanRing(raw));
|
||||
if (ring.length < 4) return; // fewer than 3 distinct vertices
|
||||
const wantClockwise = i === 0; // ring 0 is the exterior
|
||||
// Shoelace: positive area means counter-clockwise.
|
||||
if ((signedArea(ring) > 0) === wantClockwise) ring.reverse();
|
||||
out.push(ring);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function closeRing(ring) {
|
||||
if (ring.length < 3) return ring;
|
||||
const first = ring[0], last = ring[ring.length - 1];
|
||||
if (first[0] !== last[0] || first[1] !== last[1]) ring.push([first[0], first[1]]);
|
||||
return ring;
|
||||
}
|
||||
|
||||
function signedArea(ring) {
|
||||
let sum = 0;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
sum += (ring[j][0] * ring[i][1]) - (ring[i][0] * ring[j][1]);
|
||||
}
|
||||
return sum / 2;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// .shp / .shx
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Write the geometry (.shp) and its index (.shx). The two share a 100-byte
|
||||
* header; the index holds one 8-byte offset/length pair per record.
|
||||
*/
|
||||
function buildShpShx(records, shapeType) {
|
||||
const isPoint = shapeType === SHP_POINT;
|
||||
|
||||
// Measure first so both buffers can be allocated exactly.
|
||||
const contentLengths = records.map((r) =>
|
||||
isPoint ? 20 : 44 + 4 * r.parts.length + 16 * countPoints(r.parts)
|
||||
);
|
||||
const shpLength = contentLengths.reduce((n, len) => n + 8 + len, 100);
|
||||
const shxLength = 100 + 8 * records.length;
|
||||
|
||||
const shp = new DataView(new ArrayBuffer(shpLength));
|
||||
const shx = new DataView(new ArrayBuffer(shxLength));
|
||||
const box = boundingBox(records);
|
||||
|
||||
writeShpHeader(shp, shapeType, shpLength, box);
|
||||
writeShpHeader(shx, shapeType, shxLength, box);
|
||||
|
||||
let offset = 100;
|
||||
records.forEach((r, i) => {
|
||||
const len = contentLengths[i];
|
||||
|
||||
// Record header: 1-based number and content length, both big-endian, in
|
||||
// 16-bit words — the only big-endian fields in the format.
|
||||
shp.setInt32(offset, i + 1);
|
||||
shp.setInt32(offset + 4, len / 2);
|
||||
|
||||
// .shx entry points at the record *header*, not its content.
|
||||
shx.setInt32(100 + i * 8, offset / 2);
|
||||
shx.setInt32(100 + i * 8 + 4, len / 2);
|
||||
|
||||
writeRecordContent(shp, offset + 8, r, shapeType);
|
||||
offset += 8 + len;
|
||||
});
|
||||
|
||||
return { shp: new Uint8Array(shp.buffer), shx: new Uint8Array(shx.buffer) };
|
||||
}
|
||||
|
||||
function writeShpHeader(view, shapeType, byteLength, box) {
|
||||
view.setInt32(0, 9994); // file code (big-endian)
|
||||
view.setInt32(24, byteLength / 2); // length in 16-bit words
|
||||
view.setInt32(28, 1000, true); // version
|
||||
view.setInt32(32, shapeType, true);
|
||||
view.setFloat64(36, box.xmin, true);
|
||||
view.setFloat64(44, box.ymin, true);
|
||||
view.setFloat64(52, box.xmax, true);
|
||||
view.setFloat64(60, box.ymax, true);
|
||||
// Bytes 68..99 are the Z and M ranges, left at zero for 2D shapes.
|
||||
}
|
||||
|
||||
function writeRecordContent(view, at, record, shapeType) {
|
||||
if (shapeType === SHP_POINT) {
|
||||
const [x, y] = record.parts[0][0];
|
||||
view.setInt32(at, SHP_POINT, true);
|
||||
view.setFloat64(at + 4, x, true);
|
||||
view.setFloat64(at + 12, y, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const box = boundingBox([record]);
|
||||
const numPoints = countPoints(record.parts);
|
||||
|
||||
view.setInt32(at, shapeType, true);
|
||||
view.setFloat64(at + 4, box.xmin, true);
|
||||
view.setFloat64(at + 12, box.ymin, true);
|
||||
view.setFloat64(at + 20, box.xmax, true);
|
||||
view.setFloat64(at + 28, box.ymax, true);
|
||||
view.setInt32(at + 36, record.parts.length, true);
|
||||
view.setInt32(at + 40, numPoints, true);
|
||||
|
||||
// Part index: the position, in the flat point list, where each part starts.
|
||||
let partsAt = at + 44;
|
||||
let start = 0;
|
||||
for (const part of record.parts) {
|
||||
view.setInt32(partsAt, start, true);
|
||||
partsAt += 4;
|
||||
start += part.length;
|
||||
}
|
||||
|
||||
let pointsAt = partsAt;
|
||||
for (const part of record.parts) {
|
||||
for (const [x, y] of part) {
|
||||
view.setFloat64(pointsAt, x, true);
|
||||
view.setFloat64(pointsAt + 8, y, true);
|
||||
pointsAt += 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countPoints(parts) {
|
||||
return parts.reduce((n, p) => n + p.length, 0);
|
||||
}
|
||||
|
||||
function boundingBox(records) {
|
||||
let xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
|
||||
for (const r of records) {
|
||||
for (const part of r.parts) {
|
||||
for (const [x, y] of part) {
|
||||
if (x < xmin) xmin = x;
|
||||
if (y < ymin) ymin = y;
|
||||
if (x > xmax) xmax = x;
|
||||
if (y > ymax) ymax = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(xmin)) return { xmin: 0, ymin: 0, xmax: 0, ymax: 0 };
|
||||
return { xmin, ymin, xmax, ymax };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// .dbf
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DBF_MAX_CHAR = 254; // dBASE III character field limit
|
||||
const DBF_MAX_NUMERIC = 18; // digits a numeric field can hold safely
|
||||
const DBF_MAX_DECIMALS = 10;
|
||||
|
||||
/**
|
||||
* Write the attribute table as a dBASE III .dbf.
|
||||
*
|
||||
* Column types are inferred from the values: all-boolean → L, all-numeric → N,
|
||||
* anything else → C. Field names are sanitised to DBF's 10-character
|
||||
* alphanumeric limit and de-duplicated.
|
||||
*/
|
||||
function buildDbf(rows) {
|
||||
const fields = inferFields(rows);
|
||||
const enc = new TextEncoder();
|
||||
|
||||
const headerLength = 32 + 32 * fields.length + 1;
|
||||
const recordLength = fields.reduce((n, f) => n + f.size, 1); // +1 delete flag
|
||||
const total = headerLength + rows.length * recordLength + 1; // +1 EOF marker
|
||||
|
||||
const buf = new ArrayBuffer(total);
|
||||
const view = new DataView(buf);
|
||||
const bytes = new Uint8Array(buf);
|
||||
const now = new Date();
|
||||
|
||||
view.setUint8(0, 0x03); // dBASE III, no memo
|
||||
view.setUint8(1, now.getFullYear() - 1900);
|
||||
view.setUint8(2, now.getMonth() + 1);
|
||||
view.setUint8(3, now.getDate());
|
||||
view.setUint32(4, rows.length, true);
|
||||
view.setUint16(8, headerLength, true);
|
||||
view.setUint16(10, recordLength, true);
|
||||
|
||||
fields.forEach((f, i) => {
|
||||
const at = 32 + i * 32;
|
||||
// Name: up to 10 ASCII bytes, null-terminated within an 11-byte slot.
|
||||
const name = enc.encode(f.name).subarray(0, 10);
|
||||
bytes.set(name, at);
|
||||
view.setUint8(at + 11, f.type.charCodeAt(0));
|
||||
view.setUint8(at + 16, f.size);
|
||||
view.setUint8(at + 17, f.decimals);
|
||||
});
|
||||
view.setUint8(32 + fields.length * 32, 0x0D); // field descriptor terminator
|
||||
|
||||
let at = headerLength;
|
||||
rows.forEach((row, i) => {
|
||||
view.setUint8(at, 0x20); // 0x20 = record is live
|
||||
at += 1;
|
||||
for (const f of fields) {
|
||||
const value = f.sequential ? i + 1 : (row ? row[f.source] : undefined);
|
||||
bytes.set(formatDbfValue(enc, f, value), at);
|
||||
at += f.size;
|
||||
}
|
||||
});
|
||||
view.setUint8(at, 0x1A); // end-of-file marker
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** Decide a name, type and width for every column across all rows. */
|
||||
function inferFields(rows) {
|
||||
const names = [];
|
||||
for (const row of rows) {
|
||||
for (const k of Object.keys(row || {})) if (!names.includes(k)) names.push(k);
|
||||
}
|
||||
|
||||
// A shapefile with no attributes at all is awkward for some readers, so fall
|
||||
// back to a sequential id column.
|
||||
if (names.length === 0) {
|
||||
return [{ name: 'FID', source: null, type: 'N', size: 9, decimals: 0, sequential: true }];
|
||||
}
|
||||
|
||||
const used = new Set();
|
||||
return names.map((source) => {
|
||||
const field = describeColumn(rows.map((r) => (r ? r[source] : undefined)));
|
||||
field.source = source;
|
||||
field.name = uniqueFieldName(source, used);
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
/** DBF names are ≤10 chars of [A-Za-z0-9_] and must be unique in the table. */
|
||||
function uniqueFieldName(source, used) {
|
||||
const base = String(source).replace(/[^A-Za-z0-9_]+/g, '_').slice(0, 10) || 'field';
|
||||
let candidate = base;
|
||||
let i = 1;
|
||||
while (used.has(candidate.toUpperCase())) {
|
||||
const suffix = String(i++);
|
||||
candidate = base.slice(0, Math.max(1, 10 - suffix.length)) + suffix;
|
||||
}
|
||||
used.add(candidate.toUpperCase());
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function describeColumn(values) {
|
||||
const enc = new TextEncoder();
|
||||
let sawValue = false, allBool = true, allNumber = true;
|
||||
let decimals = 0, numericWidth = 1, charWidth = 1;
|
||||
|
||||
for (const v of values) {
|
||||
if (v === null || v === undefined || v === '') continue;
|
||||
sawValue = true;
|
||||
|
||||
if (typeof v !== 'boolean') allBool = false;
|
||||
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
const d = decimalsNeeded(v);
|
||||
if (d > decimals) decimals = d;
|
||||
} else {
|
||||
allNumber = false;
|
||||
}
|
||||
|
||||
const text = v instanceof Date ? v.toISOString() : String(v);
|
||||
charWidth = Math.max(charWidth, enc.encode(text).length);
|
||||
}
|
||||
|
||||
if (!sawValue) return { type: 'C', size: 1, decimals: 0 };
|
||||
if (allBool) return { type: 'L', size: 1, decimals: 0 };
|
||||
|
||||
if (allNumber) {
|
||||
// Width must cover the widest value once every value is rendered at the
|
||||
// column's shared decimal count.
|
||||
let representable = true;
|
||||
for (const v of values) {
|
||||
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
||||
const text = plainFixed(v, decimals);
|
||||
if (text === null) { representable = false; break; }
|
||||
numericWidth = Math.max(numericWidth, text.length);
|
||||
}
|
||||
if (representable && numericWidth <= DBF_MAX_NUMERIC) {
|
||||
return { type: 'N', size: numericWidth, decimals };
|
||||
}
|
||||
// Too wide for a numeric column (huge magnitudes) — keep it as text, where
|
||||
// String(v) at least stays readable instead of silently losing digits.
|
||||
}
|
||||
|
||||
return { type: 'C', size: Math.min(charWidth, DBF_MAX_CHAR), decimals: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a number as plain decimal text, or null if it cannot be.
|
||||
*
|
||||
* DBF numerics are ASCII digits, but Number#toFixed switches to exponential
|
||||
* notation at 1e21 — writing "1.234e+25" into an N column produces a field no
|
||||
* reader can parse. Such values belong in a character column instead.
|
||||
*/
|
||||
function plainFixed(v, decimals) {
|
||||
const s = v.toFixed(decimals);
|
||||
return (s.includes('e') || s.includes('E')) ? null : s;
|
||||
}
|
||||
|
||||
/** Fewest decimal places that still round-trip the value, capped. */
|
||||
function decimalsNeeded(v) {
|
||||
if (Number.isInteger(v)) return 0;
|
||||
for (let d = 1; d < DBF_MAX_DECIMALS; d++) {
|
||||
if (Number(v.toFixed(d)) === v) return d;
|
||||
}
|
||||
return DBF_MAX_DECIMALS;
|
||||
}
|
||||
|
||||
/** Render one value into exactly `field.size` bytes. */
|
||||
function formatDbfValue(enc, field, value) {
|
||||
const out = new Uint8Array(field.size).fill(0x20); // space-padded
|
||||
|
||||
if (field.type === 'L') {
|
||||
out[0] = value === null || value === undefined || value === ''
|
||||
? 0x3F // '?' = unknown
|
||||
: (value ? 0x54 : 0x46); // 'T' / 'F'
|
||||
return out;
|
||||
}
|
||||
|
||||
if (value === null || value === undefined || value === '') return out;
|
||||
|
||||
if (field.type === 'N') {
|
||||
const text = typeof value === 'number' && Number.isFinite(value)
|
||||
? plainFixed(value, field.decimals)
|
||||
: null;
|
||||
// Numerics are right-justified; anything that still overflows is left
|
||||
// blank rather than truncated, since half a number is worse than none.
|
||||
const b = text === null ? null : enc.encode(text);
|
||||
if (b && b.length && b.length <= field.size) out.set(b, field.size - b.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
const text = value instanceof Date ? value.toISOString() : String(value);
|
||||
out.set(truncateUtf8(enc, text, field.size), 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Encode to at most `max` bytes without splitting a multi-byte character. */
|
||||
function truncateUtf8(enc, text, max) {
|
||||
const bytes = enc.encode(text);
|
||||
if (bytes.length <= max) return bytes;
|
||||
let end = max;
|
||||
while (end > 0 && (bytes[end] & 0xC0) === 0x80) end--; // back off continuation bytes
|
||||
return bytes.subarray(0, end);
|
||||
}
|
||||
112
src/zip-store.js
Normal file
112
src/zip-store.js
Normal file
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@ -63,9 +63,10 @@ export default defineConfig({
|
||||
if (id.includes('node_modules/ol/')) return 'openlayers';
|
||||
if (id.includes('node_modules/ol-ext/')) return 'ol-ext';
|
||||
if (id.includes('node_modules/bootstrap/')) return 'bootstrap';
|
||||
// shpjs (+ its jszip dependency) — dynamically imported at runtime,
|
||||
// so this chunk is only fetched when the user imports a shapefile.
|
||||
if (id.includes('node_modules/shpjs/') || id.includes('node_modules/jszip/')) return 'shpjs';
|
||||
// shpjs — dynamically imported at runtime, so this chunk is only
|
||||
// fetched when the user imports a shapefile. (Writing shapefiles
|
||||
// needs no dependency at all; see src/shapefile-write.js.)
|
||||
if (id.includes('node_modules/shpjs/')) return 'shpjs';
|
||||
if (id.includes('node_modules/jspdf')) return 'jspdf';
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user