diff --git a/LUPMIS2_Development_Status_Report.docx b/LUPMIS2_Development_Status_Report.docx new file mode 100644 index 0000000..b9ebcd9 Binary files /dev/null and b/LUPMIS2_Development_Status_Report.docx differ diff --git a/OpenTopography_Workflow.png b/OpenTopography_Workflow.png new file mode 100644 index 0000000..66ca944 Binary files /dev/null and b/OpenTopography_Workflow.png differ diff --git a/OpenTopography_Workflow.svg b/OpenTopography_Workflow.svg new file mode 100644 index 0000000..918423e --- /dev/null +++ b/OpenTopography_Workflow.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + OpenTopography → LUPMIS2 Topographic Workflow + + + One-off ETL pipeline: download DEM → generate products → serve to the PWA + + + + + + + + + OpenTopography API + SRTM 30m / Copernicus 30m DEM + API key required · 50 calls/24h (non-academic) + + + + one-off download · Ghana bbox ≈ 240,000 km² + + + + + + + + DEM GeoTIFF + Ghana elevation raster (single file) + EPSG:4326 · ≈ 1 – 3 GB + + + + + contours path + hillshade path + + + + + + + + + gdal_contour + extract contour polylines at fixed intervals + -i 10 (10 m) or -i 20 (20 m) + + + + + + gdaldem hillshade + render shaded relief PNG + -z 2 -az 315 -alt 45 + + + + + + + + + + + + Contour polylines + Shapefile / GeoPackage / GeoJSON + vector + + + + + Hillshade raster + GeoTIFF / PNG tile pyramid + raster + + + + + + + + + + + + GeoServer + WMS endpoint + on-demand rendering + + + + + MBTiles + XYZ tile server + pre-rendered, fast + + + + + Tile pyramid + XYZ / WMTS + gdal2tiles.py + + + + + + + + + serve as WMS or XYZ + + + + + + + + + + + + + + LUPMIS2 PWA + OpenLayers · addWMSLayer() / addXYZLayer() + "Biophysical Environment" group + + + + + + + Legend + + External source + + Raw data file + + Processing step + + Derived product + + Serving layer + + Consumer + Run once · serve forever + + + + + Prepared for LUSPA · April 2026 · One-off ETL job — no runtime OpenTopography API calls from the PWA + + diff --git a/Topographic_Background_Layers_for_LUPMIS2.docx b/Topographic_Background_Layers_for_LUPMIS2.docx new file mode 100644 index 0000000..693037e Binary files /dev/null and b/Topographic_Background_Layers_for_LUPMIS2.docx differ diff --git a/dist/assets/html2canvas.esm-B0tyYwQk.js b/dist/assets/html2canvas.esm-B0tyYwQk.js new file mode 100644 index 0000000..d873e29 --- /dev/null +++ b/dist/assets/html2canvas.esm-B0tyYwQk.js @@ -0,0 +1,23 @@ +/*! + * html2canvas 1.4.1 + * Copyright (c) 2022 Niklas von Hertzen + * Released under MIT License + *//*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var mr=function(e,A){return mr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var B in r)Object.prototype.hasOwnProperty.call(r,B)&&(t[B]=r[B])},mr(e,A)};function tA(e,A){if(typeof A!="function"&&A!==null)throw new TypeError("Class extends value "+String(A)+" is not a constructor or null");mr(e,A);function t(){this.constructor=e}e.prototype=A===null?Object.create(A):(t.prototype=A.prototype,new t)}var Lr=function(){return Lr=Object.assign||function(A){for(var t,r=1,B=arguments.length;r0&&n[n.length-1])&&(o[0]===6||o[0]===2)){t=0;continue}if(o[0]===3&&(!n||o[1]>n[0]&&o[1]=55296&&B<=56319&&t>10)+55296,s%1024+56320)),(B+1===t||r.length>16384)&&(n+=String.fromCharCode.apply(String,r),r.length=0)}return n},nt="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",on=typeof Uint8Array>"u"?[]:new Uint8Array(256);for(var le=0;le"u"?[]:new Uint8Array(256);for(var fe=0;fe>4,Q[B++]=(s&15)<<4|i>>2,Q[B++]=(i&3)<<6|a&63;return o},gn=function(e){for(var A=e.length,t=[],r=0;r>bA,Cn=1<>bA,ln=BB+un,fn=ln,Un=32,Fn=fn+Un,hn=65536>>zr,dn=1<=0){if(A<55296||A>56319&&A<=65535)return t=this.index[A>>bA],t=(t<>bA)],t=(t<>zr),t=this.index[t],t+=A>>bA&En,t=this.index[t],t=(t<"u"?[]:new Uint8Array(256);for(var Ue=0;Ueot?(B.push(!0),i-=ot):B.push(!1),["normal","auto","loose"].indexOf(A)!==-1&&[8208,8211,12316,12448].indexOf(n)!==-1)return r.push(s),t.push(xr);if(i===mn||i===Dr){if(s===0)return r.push(s),t.push(LA);var a=t[s-1];return Mn.indexOf(a)===-1?(r.push(r[s-1]),t.push(a)):(r.push(s),t.push(LA))}if(r.push(s),i===Tn)return t.push(A==="strict"?Tr:Qe);if(i===iB||i===xn)return t.push(LA);if(i===Sn)return n>=131072&&n<=196605||n>=196608&&n<=262141?t.push(Qe):t.push(LA);t.push(i)}),[r,t,B]},wr=function(e,A,t,r){var B=r[t];if(Array.isArray(e)?e.indexOf(B)!==-1:e===B)for(var n=t;n<=r.length;){n++;var s=r[n];if(s===A)return!0;if(s!==FA)break}if(B===FA)for(var n=t;n>0;){n--;var i=r[n];if(Array.isArray(e)?e.indexOf(i)!==-1:e===i)for(var a=t;a<=r.length;){a++;var s=r[a];if(s===A)return!0;if(s!==FA)break}if(i!==FA)break}return!1},lt=function(e,A){for(var t=e;t>=0;){var r=A[t];if(r===FA)t--;else return r}return 0},Vn=function(e,A,t,r,B){if(t[r]===0)return I;var n=r-1;if(Array.isArray(B)&&B[n]===!0)return I;var s=n-1,i=n+1,a=A[n],o=s>=0?A[s]:0,Q=A[i];if(a===nB&&Q===sB)return I;if(Vr.indexOf(a)!==-1)return oB;if(Vr.indexOf(Q)!==-1||QB.indexOf(Q)!==-1)return I;if(lt(n,A)===aB)return Ee;if(Rr.get(e[n])===Dr||(a===he||a===de)&&Rr.get(e[i])===Dr||a===Qt||Q===Qt||a===gt||[FA,br,$A].indexOf(a)===-1&&Q===gt||[Fe,WA,bn,SA,OA].indexOf(Q)!==-1||lt(n,A)===ZA||wr(Qr,ZA,n,A)||wr([Fe,WA],Tr,n,A)||wr(wt,wt,n,A))return I;if(a===FA)return Ee;if(a===Qr||Q===Qr)return I;if(Q===xr||a===xr)return Ee;if([br,$A,Tr].indexOf(Q)!==-1||a===Dn||o===Mr&&Gn.indexOf(a)!==-1||a===OA&&Q===Mr||Q===ct||QA.indexOf(Q)!==-1&&a===j||QA.indexOf(a)!==-1&&Q===j||a===ee&&[Qe,he,de].indexOf(Q)!==-1||[Qe,he,de].indexOf(a)!==-1&&Q===Ae||QA.indexOf(a)!==-1&&Ct.indexOf(Q)!==-1||Ct.indexOf(a)!==-1&&QA.indexOf(Q)!==-1||[ee,Ae].indexOf(a)!==-1&&(Q===j||[ZA,$A].indexOf(Q)!==-1&&A[i+1]===j)||[ZA,$A].indexOf(a)!==-1&&Q===j||a===j&&[j,OA,SA].indexOf(Q)!==-1)return I;if([j,OA,SA,Fe,WA].indexOf(Q)!==-1)for(var g=n;g>=0;){var w=A[g];if(w===j)return I;if([OA,SA].indexOf(w)!==-1)g--;else break}if([ee,Ae].indexOf(Q)!==-1)for(var g=[Fe,WA].indexOf(a)!==-1?s:n;g>=0;){var w=A[g];if(w===j)return I;if([OA,SA].indexOf(w)!==-1)g--;else break}if(Gr===a&&[Gr,Ge,Sr,Or].indexOf(Q)!==-1||[Ge,Sr].indexOf(a)!==-1&&[Ge,Re].indexOf(Q)!==-1||[Re,Or].indexOf(a)!==-1&&Q===Re||ut.indexOf(a)!==-1&&[ct,Ae].indexOf(Q)!==-1||ut.indexOf(Q)!==-1&&a===ee||QA.indexOf(a)!==-1&&QA.indexOf(Q)!==-1||a===SA&&QA.indexOf(Q)!==-1||QA.concat(j).indexOf(a)!==-1&&Q===ZA&&On.indexOf(e[i])===-1||QA.concat(j).indexOf(Q)!==-1&&a===WA)return I;if(a===gr&&Q===gr){for(var f=t[n],c=1;f>0&&(f--,A[f]===gr);)c++;if(c%2!==0)return I}return a===he&&Q===de?I:Ee},Nn=function(e,A){A||(A={lineBreak:"normal",wordBreak:"normal"});var t=Rn(e,A.lineBreak),r=t[0],B=t[1],n=t[2];(A.wordBreak==="break-all"||A.wordBreak==="break-word")&&(B=B.map(function(i){return[j,LA,iB].indexOf(i)!==-1?Qe:i}));var s=A.wordBreak==="keep-all"?n.map(function(i,a){return i&&e[a]>=19968&&e[a]<=40959}):void 0;return[r,B,s]},Xn=(function(){function e(A,t,r,B){this.codePoints=A,this.required=t===oB,this.start=r,this.end=B}return e.prototype.slice=function(){return S.apply(void 0,this.codePoints.slice(this.start,this.end))},e})(),_n=function(e,A){var t=$e(e),r=Nn(t,A),B=r[0],n=r[1],s=r[2],i=t.length,a=0,o=0;return{next:function(){if(o>=i)return{done:!0,value:null};for(var Q=I;o=gB&&e<=57},Hs=function(e){return e>=55296&&e<=57343},MA=function(e){return J(e)||e>=CB&&e<=lB||e>=wB&&e<=Us},ps=function(e){return e>=wB&&e<=hs},Is=function(e){return e>=CB&&e<=Es},vs=function(e){return ps(e)||Is(e)},ys=function(e){return e>=is},ye=function(e){return e===Xe||e===kn||e===Yn},_e=function(e){return vs(e)||ys(e)||e===jn},dt=function(e){return _e(e)||J(e)||e===Z},Ks=function(e){return e>=ws&&e<=cs||e===Cs||e>=us&&e<=ls||e===fs},UA=function(e,A){return e!==ne?!1:A!==Xe},Ke=function(e,A,t){return e===Z?_e(A)||UA(A,t):_e(e)?!0:!!(e===ne&&UA(e,A))},Cr=function(e,A,t){return e===DA||e===Z?J(A)?!0:A===ge&&J(t):J(e===ge?A:e)},ms=function(e){var A=0,t=1;(e[A]===DA||e[A]===Z)&&(e[A]===Z&&(t=-1),A++);for(var r=[];J(e[A]);)r.push(e[A++]);var B=r.length?parseInt(S.apply(void 0,r),10):0;e[A]===ge&&A++;for(var n=[];J(e[A]);)n.push(e[A++]);var s=n.length,i=s?parseInt(S.apply(void 0,n),10):0;(e[A]===uB||e[A]===cB)&&A++;var a=1;(e[A]===DA||e[A]===Z)&&(e[A]===Z&&(a=-1),A++);for(var o=[];J(e[A]);)o.push(e[A++]);var Q=o.length?parseInt(S.apply(void 0,o),10):0;return t*(B+i*Math.pow(10,-s))*Math.pow(10,a*Q)},Ls={type:2},Ds={type:3},bs={type:4},xs={type:13},Ts={type:8},Ss={type:21},Os={type:9},Ms={type:10},Gs={type:11},Rs={type:12},Vs={type:14},me={type:23},Ns={type:1},Xs={type:25},_s={type:24},Js={type:26},Ps={type:27},ks={type:28},Ys={type:29},Ws={type:31},Nr={type:32},fB=(function(){function e(){this._value=[]}return e.prototype.write=function(A){this._value=this._value.concat($e(A))},e.prototype.read=function(){for(var A=[],t=this.consumeToken();t!==Nr;)A.push(t),t=this.consumeToken();return A},e.prototype.consumeToken=function(){var A=this.consumeCodePoint();switch(A){case He:return this.consumeStringToken(He);case Wn:var t=this.peekCodePoint(0),r=this.peekCodePoint(1),B=this.peekCodePoint(2);if(dt(t)||UA(r,B)){var n=Ke(t,r,B)?Pn:Jn,s=this.consumeName();return{type:5,value:s,flags:n}}break;case Zn:if(this.peekCodePoint(0)===qA)return this.consumeCodePoint(),xs;break;case pe:return this.consumeStringToken(pe);case Ie:return Ls;case jA:return Ds;case cr:if(this.peekCodePoint(0)===qA)return this.consumeCodePoint(),Vs;break;case DA:if(Cr(A,this.peekCodePoint(0),this.peekCodePoint(1)))return this.reconsumeCodePoint(A),this.consumeNumericToken();break;case os:return bs;case Z:var i=A,a=this.peekCodePoint(0),o=this.peekCodePoint(1);if(Cr(i,a,o))return this.reconsumeCodePoint(A),this.consumeNumericToken();if(Ke(i,a,o))return this.reconsumeCodePoint(A),this.consumeIdentLikeToken();if(a===Z&&o===As)return this.consumeCodePoint(),this.consumeCodePoint(),_s;break;case ge:if(Cr(A,this.peekCodePoint(0),this.peekCodePoint(1)))return this.reconsumeCodePoint(A),this.consumeNumericToken();break;case Ut:if(this.peekCodePoint(0)===cr)for(this.consumeCodePoint();;){var Q=this.consumeCodePoint();if(Q===cr&&(Q=this.consumeCodePoint(),Q===Ut))return this.consumeToken();if(Q===nA)return this.consumeToken()}break;case Qs:return Js;case gs:return Ps;case $n:if(this.peekCodePoint(0)===zn&&this.peekCodePoint(1)===Z&&this.peekCodePoint(2)===Z)return this.consumeCodePoint(),this.consumeCodePoint(),Xs;break;case es:var g=this.peekCodePoint(0),w=this.peekCodePoint(1),f=this.peekCodePoint(2);if(Ke(g,w,f)){var s=this.consumeName();return{type:7,value:s}}break;case rs:return ks;case ne:if(UA(A,this.peekCodePoint(0)))return this.reconsumeCodePoint(A),this.consumeIdentLikeToken();break;case ts:return Ys;case Bs:if(this.peekCodePoint(0)===qA)return this.consumeCodePoint(),Ts;break;case ns:return Gs;case ss:return Rs;case Fs:case ds:var c=this.peekCodePoint(0),C=this.peekCodePoint(1);return c===DA&&(MA(C)||C===ve)&&(this.consumeCodePoint(),this.consumeUnicodeRangeToken()),this.reconsumeCodePoint(A),this.consumeIdentLikeToken();case Ft:if(this.peekCodePoint(0)===qA)return this.consumeCodePoint(),Os;if(this.peekCodePoint(0)===Ft)return this.consumeCodePoint(),Ss;break;case as:if(this.peekCodePoint(0)===qA)return this.consumeCodePoint(),Ms;break;case nA:return Nr}return ye(A)?(this.consumeWhiteSpace(),Ws):J(A)?(this.reconsumeCodePoint(A),this.consumeNumericToken()):_e(A)?(this.reconsumeCodePoint(A),this.consumeIdentLikeToken()):{type:6,value:S(A)}},e.prototype.consumeCodePoint=function(){var A=this._value.shift();return typeof A>"u"?-1:A},e.prototype.reconsumeCodePoint=function(A){this._value.unshift(A)},e.prototype.peekCodePoint=function(A){return A>=this._value.length?-1:this._value[A]},e.prototype.consumeUnicodeRangeToken=function(){for(var A=[],t=this.consumeCodePoint();MA(t)&&A.length<6;)A.push(t),t=this.consumeCodePoint();for(var r=!1;t===ve&&A.length<6;)A.push(t),t=this.consumeCodePoint(),r=!0;if(r){var B=parseInt(S.apply(void 0,A.map(function(a){return a===ve?gB:a})),16),n=parseInt(S.apply(void 0,A.map(function(a){return a===ve?lB:a})),16);return{type:30,start:B,end:n}}var s=parseInt(S.apply(void 0,A),16);if(this.peekCodePoint(0)===Z&&MA(this.peekCodePoint(1))){this.consumeCodePoint(),t=this.consumeCodePoint();for(var i=[];MA(t)&&i.length<6;)i.push(t),t=this.consumeCodePoint();var n=parseInt(S.apply(void 0,i),16);return{type:30,start:s,end:n}}else return{type:30,start:s,end:s}},e.prototype.consumeIdentLikeToken=function(){var A=this.consumeName();return A.toLowerCase()==="url"&&this.peekCodePoint(0)===Ie?(this.consumeCodePoint(),this.consumeUrlToken()):this.peekCodePoint(0)===Ie?(this.consumeCodePoint(),{type:19,value:A}):{type:20,value:A}},e.prototype.consumeUrlToken=function(){var A=[];if(this.consumeWhiteSpace(),this.peekCodePoint(0)===nA)return{type:22,value:""};var t=this.peekCodePoint(0);if(t===pe||t===He){var r=this.consumeStringToken(this.consumeCodePoint());return r.type===0&&(this.consumeWhiteSpace(),this.peekCodePoint(0)===nA||this.peekCodePoint(0)===jA)?(this.consumeCodePoint(),{type:22,value:r.value}):(this.consumeBadUrlRemnants(),me)}for(;;){var B=this.consumeCodePoint();if(B===nA||B===jA)return{type:22,value:S.apply(void 0,A)};if(ye(B))return this.consumeWhiteSpace(),this.peekCodePoint(0)===nA||this.peekCodePoint(0)===jA?(this.consumeCodePoint(),{type:22,value:S.apply(void 0,A)}):(this.consumeBadUrlRemnants(),me);if(B===He||B===pe||B===Ie||Ks(B))return this.consumeBadUrlRemnants(),me;if(B===ne)if(UA(B,this.peekCodePoint(0)))A.push(this.consumeEscapedCodePoint());else return this.consumeBadUrlRemnants(),me;else A.push(B)}},e.prototype.consumeWhiteSpace=function(){for(;ye(this.peekCodePoint(0));)this.consumeCodePoint()},e.prototype.consumeBadUrlRemnants=function(){for(;;){var A=this.consumeCodePoint();if(A===jA||A===nA)return;UA(A,this.peekCodePoint(0))&&this.consumeEscapedCodePoint()}},e.prototype.consumeStringSlice=function(A){for(var t=5e4,r="";A>0;){var B=Math.min(t,A);r+=S.apply(void 0,this._value.splice(0,B)),A-=B}return this._value.shift(),r},e.prototype.consumeStringToken=function(A){var t="",r=0;do{var B=this._value[r];if(B===nA||B===void 0||B===A)return t+=this.consumeStringSlice(r),{type:0,value:t};if(B===Xe)return this._value.splice(0,r),Ns;if(B===ne){var n=this._value[r+1];n!==nA&&n!==void 0&&(n===Xe?(t+=this.consumeStringSlice(r),r=-1,this._value.shift()):UA(B,n)&&(t+=this.consumeStringSlice(r),t+=S(this.consumeEscapedCodePoint()),r=-1))}r++}while(!0)},e.prototype.consumeNumber=function(){var A=[],t=ce,r=this.peekCodePoint(0);for((r===DA||r===Z)&&A.push(this.consumeCodePoint());J(this.peekCodePoint(0));)A.push(this.consumeCodePoint());r=this.peekCodePoint(0);var B=this.peekCodePoint(1);if(r===ge&&J(B))for(A.push(this.consumeCodePoint(),this.consumeCodePoint()),t=ft;J(this.peekCodePoint(0));)A.push(this.consumeCodePoint());r=this.peekCodePoint(0),B=this.peekCodePoint(1);var n=this.peekCodePoint(2);if((r===uB||r===cB)&&((B===DA||B===Z)&&J(n)||J(B)))for(A.push(this.consumeCodePoint(),this.consumeCodePoint()),t=ft;J(this.peekCodePoint(0));)A.push(this.consumeCodePoint());return[ms(A),t]},e.prototype.consumeNumericToken=function(){var A=this.consumeNumber(),t=A[0],r=A[1],B=this.peekCodePoint(0),n=this.peekCodePoint(1),s=this.peekCodePoint(2);if(Ke(B,n,s)){var i=this.consumeName();return{type:15,number:t,flags:r,unit:i}}return B===qn?(this.consumeCodePoint(),{type:16,number:t,flags:r}):{type:17,number:t,flags:r}},e.prototype.consumeEscapedCodePoint=function(){var A=this.consumeCodePoint();if(MA(A)){for(var t=S(A);MA(this.peekCodePoint(0))&&t.length<6;)t+=S(this.consumeCodePoint());ye(this.peekCodePoint(0))&&this.consumeCodePoint();var r=parseInt(t,16);return r===0||Hs(r)||r>1114111?ht:r}return A===nA?ht:A},e.prototype.consumeName=function(){for(var A="";;){var t=this.consumeCodePoint();if(dt(t))A+=S(t);else if(UA(t,this.peekCodePoint(0)))A+=S(this.consumeEscapedCodePoint());else return this.reconsumeCodePoint(t),A}},e})(),UB=(function(){function e(A){this._tokens=A}return e.create=function(A){var t=new fB;return t.write(A),new e(t.read())},e.parseValue=function(A){return e.create(A).parseComponentValue()},e.parseValues=function(A){return e.create(A).parseComponentValues()},e.prototype.parseComponentValue=function(){for(var A=this.consumeToken();A.type===31;)A=this.consumeToken();if(A.type===32)throw new SyntaxError("Error parsing CSS component value, unexpected EOF");this.reconsumeToken(A);var t=this.consumeComponentValue();do A=this.consumeToken();while(A.type===31);if(A.type===32)return t;throw new SyntaxError("Error parsing CSS component value, multiple values found when expecting only one")},e.prototype.parseComponentValues=function(){for(var A=[];;){var t=this.consumeComponentValue();if(t.type===32)return A;A.push(t),A.push()}},e.prototype.consumeComponentValue=function(){var A=this.consumeToken();switch(A.type){case 11:case 28:case 2:return this.consumeSimpleBlock(A.type);case 19:return this.consumeFunction(A)}return A},e.prototype.consumeSimpleBlock=function(A){for(var t={type:A,values:[]},r=this.consumeToken();;){if(r.type===32||qs(r,A))return t;this.reconsumeToken(r),t.values.push(this.consumeComponentValue()),r=this.consumeToken()}},e.prototype.consumeFunction=function(A){for(var t={name:A.value,values:[],type:18};;){var r=this.consumeToken();if(r.type===32||r.type===3)return t;this.reconsumeToken(r),t.values.push(this.consumeComponentValue())}},e.prototype.consumeToken=function(){var A=this._tokens.shift();return typeof A>"u"?Nr:A},e.prototype.reconsumeToken=function(A){this._tokens.unshift(A)},e})(),Ce=function(e){return e.type===15},kA=function(e){return e.type===17},D=function(e){return e.type===20},Zs=function(e){return e.type===0},Xr=function(e,A){return D(e)&&e.value===A},FB=function(e){return e.type!==31},PA=function(e){return e.type!==31&&e.type!==4},sA=function(e){var A=[],t=[];return e.forEach(function(r){if(r.type===4){if(t.length===0)throw new Error("Error parsing function args, zero tokens for arg");A.push(t),t=[];return}r.type!==31&&t.push(r)}),t.length&&A.push(t),A},qs=function(e,A){return A===11&&e.type===12||A===28&&e.type===29?!0:A===2&&e.type===3},pA=function(e){return e.type===17||e.type===15},M=function(e){return e.type===16||pA(e)},hB=function(e){return e.length>1?[e[0],e[1]]:[e[0]]},X={type:17,number:0,flags:ce},$r={type:16,number:50,flags:ce},hA={type:16,number:100,flags:ce},re=function(e,A,t){var r=e[0],B=e[1];return[b(r,A),b(typeof B<"u"?B:r,t)]},b=function(e,A){if(e.type===16)return e.number/100*A;if(Ce(e))switch(e.unit){case"rem":case"em":return 16*e.number;case"px":default:return e.number}return e.number},dB="deg",EB="grad",HB="rad",pB="turn",Ar={name:"angle",parse:function(e,A){if(A.type===15)switch(A.unit){case dB:return Math.PI*A.number/180;case EB:return Math.PI/200*A.number;case HB:return A.number;case pB:return Math.PI*2*A.number}throw new Error("Unsupported angle type")}},IB=function(e){return e.type===15&&(e.unit===dB||e.unit===EB||e.unit===HB||e.unit===pB)},vB=function(e){var A=e.filter(D).map(function(t){return t.value}).join(" ");switch(A){case"to bottom right":case"to right bottom":case"left top":case"top left":return[X,X];case"to top":case"bottom":return AA(0);case"to bottom left":case"to left bottom":case"right top":case"top right":return[X,hA];case"to right":case"left":return AA(90);case"to top left":case"to left top":case"right bottom":case"bottom right":return[hA,hA];case"to bottom":case"top":return AA(180);case"to top right":case"to right top":case"left bottom":case"bottom left":return[hA,X];case"to left":case"right":return AA(270)}return 0},AA=function(e){return Math.PI*e/180},EA={name:"color",parse:function(e,A){if(A.type===18){var t=js[A.name];if(typeof t>"u")throw new Error('Attempting to parse an unsupported color function "'+A.name+'"');return t(e,A.values)}if(A.type===5){if(A.value.length===3){var r=A.value.substring(0,1),B=A.value.substring(1,2),n=A.value.substring(2,3);return dA(parseInt(r+r,16),parseInt(B+B,16),parseInt(n+n,16),1)}if(A.value.length===4){var r=A.value.substring(0,1),B=A.value.substring(1,2),n=A.value.substring(2,3),s=A.value.substring(3,4);return dA(parseInt(r+r,16),parseInt(B+B,16),parseInt(n+n,16),parseInt(s+s,16)/255)}if(A.value.length===6){var r=A.value.substring(0,2),B=A.value.substring(2,4),n=A.value.substring(4,6);return dA(parseInt(r,16),parseInt(B,16),parseInt(n,16),1)}if(A.value.length===8){var r=A.value.substring(0,2),B=A.value.substring(2,4),n=A.value.substring(4,6),s=A.value.substring(6,8);return dA(parseInt(r,16),parseInt(B,16),parseInt(n,16),parseInt(s,16)/255)}}if(A.type===20){var i=wA[A.value.toUpperCase()];if(typeof i<"u")return i}return wA.TRANSPARENT}},HA=function(e){return(255&e)===0},R=function(e){var A=255&e,t=255&e>>8,r=255&e>>16,B=255&e>>24;return A<255?"rgba("+B+","+r+","+t+","+A/255+")":"rgb("+B+","+r+","+t+")"},dA=function(e,A,t,r){return(e<<24|A<<16|t<<8|Math.round(r*255)<<0)>>>0},Et=function(e,A){if(e.type===17)return e.number;if(e.type===16){var t=A===3?1:255;return A===3?e.number/100*t:Math.round(e.number/100*t)}return 0},Ht=function(e,A){var t=A.filter(PA);if(t.length===3){var r=t.map(Et),B=r[0],n=r[1],s=r[2];return dA(B,n,s,1)}if(t.length===4){var i=t.map(Et),B=i[0],n=i[1],s=i[2],a=i[3];return dA(B,n,s,a)}return 0};function ur(e,A,t){return t<0&&(t+=1),t>=1&&(t-=1),t<1/6?(A-e)*t*6+e:t<1/2?A:t<2/3?(A-e)*6*(2/3-t)+e:e}var pt=function(e,A){var t=A.filter(PA),r=t[0],B=t[1],n=t[2],s=t[3],i=(r.type===17?AA(r.number):Ar.parse(e,r))/(Math.PI*2),a=M(B)?B.number/100:0,o=M(n)?n.number/100:0,Q=typeof s<"u"&&M(s)?b(s,1):1;if(a===0)return dA(o*255,o*255,o*255,1);var g=o<=.5?o*(a+1):o+a-o*a,w=o*2-g,f=ur(w,g,i+1/3),c=ur(w,g,i),C=ur(w,g,i-1/3);return dA(f*255,c*255,C*255,Q)},js={hsl:pt,hsla:pt,rgb:Ht,rgba:Ht},se=function(e,A){return EA.parse(e,UB.create(A).parseComponentValue())},wA={ALICEBLUE:4042850303,ANTIQUEWHITE:4209760255,AQUA:16777215,AQUAMARINE:2147472639,AZURE:4043309055,BEIGE:4126530815,BISQUE:4293182719,BLACK:255,BLANCHEDALMOND:4293643775,BLUE:65535,BLUEVIOLET:2318131967,BROWN:2771004159,BURLYWOOD:3736635391,CADETBLUE:1604231423,CHARTREUSE:2147418367,CHOCOLATE:3530104575,CORAL:4286533887,CORNFLOWERBLUE:1687547391,CORNSILK:4294499583,CRIMSON:3692313855,CYAN:16777215,DARKBLUE:35839,DARKCYAN:9145343,DARKGOLDENROD:3095837695,DARKGRAY:2846468607,DARKGREEN:6553855,DARKGREY:2846468607,DARKKHAKI:3182914559,DARKMAGENTA:2332068863,DARKOLIVEGREEN:1433087999,DARKORANGE:4287365375,DARKORCHID:2570243327,DARKRED:2332033279,DARKSALMON:3918953215,DARKSEAGREEN:2411499519,DARKSLATEBLUE:1211993087,DARKSLATEGRAY:793726975,DARKSLATEGREY:793726975,DARKTURQUOISE:13554175,DARKVIOLET:2483082239,DEEPPINK:4279538687,DEEPSKYBLUE:12582911,DIMGRAY:1768516095,DIMGREY:1768516095,DODGERBLUE:512819199,FIREBRICK:2988581631,FLORALWHITE:4294635775,FORESTGREEN:579543807,FUCHSIA:4278255615,GAINSBORO:3705462015,GHOSTWHITE:4177068031,GOLD:4292280575,GOLDENROD:3668254975,GRAY:2155905279,GREEN:8388863,GREENYELLOW:2919182335,GREY:2155905279,HONEYDEW:4043305215,HOTPINK:4285117695,INDIANRED:3445382399,INDIGO:1258324735,IVORY:4294963455,KHAKI:4041641215,LAVENDER:3873897215,LAVENDERBLUSH:4293981695,LAWNGREEN:2096890111,LEMONCHIFFON:4294626815,LIGHTBLUE:2916673279,LIGHTCORAL:4034953471,LIGHTCYAN:3774873599,LIGHTGOLDENRODYELLOW:4210742015,LIGHTGRAY:3553874943,LIGHTGREEN:2431553791,LIGHTGREY:3553874943,LIGHTPINK:4290167295,LIGHTSALMON:4288707327,LIGHTSEAGREEN:548580095,LIGHTSKYBLUE:2278488831,LIGHTSLATEGRAY:2005441023,LIGHTSLATEGREY:2005441023,LIGHTSTEELBLUE:2965692159,LIGHTYELLOW:4294959359,LIME:16711935,LIMEGREEN:852308735,LINEN:4210091775,MAGENTA:4278255615,MAROON:2147483903,MEDIUMAQUAMARINE:1724754687,MEDIUMBLUE:52735,MEDIUMORCHID:3126187007,MEDIUMPURPLE:2473647103,MEDIUMSEAGREEN:1018393087,MEDIUMSLATEBLUE:2070474495,MEDIUMSPRINGGREEN:16423679,MEDIUMTURQUOISE:1221709055,MEDIUMVIOLETRED:3340076543,MIDNIGHTBLUE:421097727,MINTCREAM:4127193855,MISTYROSE:4293190143,MOCCASIN:4293178879,NAVAJOWHITE:4292783615,NAVY:33023,OLDLACE:4260751103,OLIVE:2155872511,OLIVEDRAB:1804477439,ORANGE:4289003775,ORANGERED:4282712319,ORCHID:3664828159,PALEGOLDENROD:4008225535,PALEGREEN:2566625535,PALETURQUOISE:2951671551,PALEVIOLETRED:3681588223,PAPAYAWHIP:4293907967,PEACHPUFF:4292524543,PERU:3448061951,PINK:4290825215,PLUM:3718307327,POWDERBLUE:2967529215,PURPLE:2147516671,REBECCAPURPLE:1714657791,RED:4278190335,ROSYBROWN:3163525119,ROYALBLUE:1097458175,SADDLEBROWN:2336560127,SALMON:4202722047,SANDYBROWN:4104413439,SEAGREEN:780883967,SEASHELL:4294307583,SIENNA:2689740287,SILVER:3233857791,SKYBLUE:2278484991,SLATEBLUE:1784335871,SLATEGRAY:1887473919,SLATEGREY:1887473919,SNOW:4294638335,SPRINGGREEN:16744447,STEELBLUE:1182971135,TAN:3535047935,TEAL:8421631,THISTLE:3636451583,TOMATO:4284696575,TRANSPARENT:0,TURQUOISE:1088475391,VIOLET:4001558271,WHEAT:4125012991,WHITE:4294967295,WHITESMOKE:4126537215,YELLOW:4294902015,YELLOWGREEN:2597139199},zs={name:"background-clip",initialValue:"border-box",prefix:!1,type:1,parse:function(e,A){return A.map(function(t){if(D(t))switch(t.value){case"padding-box":return 1;case"content-box":return 2}return 0})}},$s={name:"background-color",initialValue:"transparent",prefix:!1,type:3,format:"color"},er=function(e,A){var t=EA.parse(e,A[0]),r=A[1];return r&&M(r)?{color:t,stop:r}:{color:t,stop:null}},It=function(e,A){var t=e[0],r=e[e.length-1];t.stop===null&&(t.stop=X),r.stop===null&&(r.stop=hA);for(var B=[],n=0,s=0;sn?B.push(a):B.push(n),n=a}else B.push(null)}for(var o=null,s=0;ss.optimumDistance)?{optimumCorner:i,optimumDistance:Q}:s},{optimumDistance:B?1/0:-1/0,optimumCorner:null}).optimumCorner},ra=function(e,A,t,r,B){var n=0,s=0;switch(e.size){case 0:e.shape===0?n=s=Math.min(Math.abs(A),Math.abs(A-r),Math.abs(t),Math.abs(t-B)):e.shape===1&&(n=Math.min(Math.abs(A),Math.abs(A-r)),s=Math.min(Math.abs(t),Math.abs(t-B)));break;case 2:if(e.shape===0)n=s=Math.min(rA(A,t),rA(A,t-B),rA(A-r,t),rA(A-r,t-B));else if(e.shape===1){var i=Math.min(Math.abs(t),Math.abs(t-B))/Math.min(Math.abs(A),Math.abs(A-r)),a=vt(r,B,A,t,!0),o=a[0],Q=a[1];n=rA(o-A,(Q-t)/i),s=i*n}break;case 1:e.shape===0?n=s=Math.max(Math.abs(A),Math.abs(A-r),Math.abs(t),Math.abs(t-B)):e.shape===1&&(n=Math.max(Math.abs(A),Math.abs(A-r)),s=Math.max(Math.abs(t),Math.abs(t-B)));break;case 3:if(e.shape===0)n=s=Math.max(rA(A,t),rA(A,t-B),rA(A-r,t),rA(A-r,t-B));else if(e.shape===1){var i=Math.max(Math.abs(t),Math.abs(t-B))/Math.max(Math.abs(A),Math.abs(A-r)),g=vt(r,B,A,t,!1),o=g[0],Q=g[1];n=rA(o-A,(Q-t)/i),s=i*n}break}return Array.isArray(e.size)&&(n=b(e.size[0],r),s=e.size.length===2?b(e.size[1],B):n),[n,s]},ta=function(e,A){var t=AA(180),r=[];return sA(A).forEach(function(B,n){if(n===0){var s=B[0];if(s.type===20&&s.value==="to"){t=vB(B);return}else if(IB(s)){t=Ar.parse(e,s);return}}var i=er(e,B);r.push(i)}),{angle:t,stops:r,type:1}},Le=function(e,A){var t=AA(180),r=[];return sA(A).forEach(function(B,n){if(n===0){var s=B[0];if(s.type===20&&["top","left","right","bottom"].indexOf(s.value)!==-1){t=vB(B);return}else if(IB(s)){t=(Ar.parse(e,s)+AA(270))%AA(360);return}}var i=er(e,B);r.push(i)}),{angle:t,stops:r,type:1}},Ba=function(e,A){var t=AA(180),r=[],B=1,n=0,s=3,i=[];return sA(A).forEach(function(a,o){var Q=a[0];if(o===0){if(D(Q)&&Q.value==="linear"){B=1;return}else if(D(Q)&&Q.value==="radial"){B=2;return}}if(Q.type===18){if(Q.name==="from"){var g=EA.parse(e,Q.values[0]);r.push({stop:X,color:g})}else if(Q.name==="to"){var g=EA.parse(e,Q.values[0]);r.push({stop:hA,color:g})}else if(Q.name==="color-stop"){var w=Q.values.filter(PA);if(w.length===2){var g=EA.parse(e,w[1]),f=w[0];kA(f)&&r.push({stop:{type:16,number:f.number*100,flags:f.flags},color:g})}}}}),B===1?{angle:(t+AA(180))%AA(360),stops:r,type:B}:{size:s,shape:n,stops:r,position:i,type:B}},yB="closest-side",KB="farthest-side",mB="closest-corner",LB="farthest-corner",DB="circle",bB="ellipse",xB="cover",TB="contain",na=function(e,A){var t=0,r=3,B=[],n=[];return sA(A).forEach(function(s,i){var a=!0;if(i===0){var o=!1;a=s.reduce(function(g,w){if(o)if(D(w))switch(w.value){case"center":return n.push($r),g;case"top":case"left":return n.push(X),g;case"right":case"bottom":return n.push(hA),g}else(M(w)||pA(w))&&n.push(w);else if(D(w))switch(w.value){case DB:return t=0,!1;case bB:return t=1,!1;case"at":return o=!0,!1;case yB:return r=0,!1;case xB:case KB:return r=1,!1;case TB:case mB:return r=2,!1;case LB:return r=3,!1}else if(pA(w)||M(w))return Array.isArray(r)||(r=[]),r.push(w),!1;return g},a)}if(a){var Q=er(e,s);B.push(Q)}}),{size:r,shape:t,stops:B,position:n,type:2}},De=function(e,A){var t=0,r=3,B=[],n=[];return sA(A).forEach(function(s,i){var a=!0;if(i===0?a=s.reduce(function(Q,g){if(D(g))switch(g.value){case"center":return n.push($r),!1;case"top":case"left":return n.push(X),!1;case"right":case"bottom":return n.push(hA),!1}else if(M(g)||pA(g))return n.push(g),!1;return Q},a):i===1&&(a=s.reduce(function(Q,g){if(D(g))switch(g.value){case DB:return t=0,!1;case bB:return t=1,!1;case TB:case yB:return r=0,!1;case KB:return r=1,!1;case mB:return r=2,!1;case xB:case LB:return r=3,!1}else if(pA(g)||M(g))return Array.isArray(r)||(r=[]),r.push(g),!1;return Q},a)),a){var o=er(e,s);B.push(o)}}),{size:r,shape:t,stops:B,position:n,type:2}},sa=function(e){return e.type===1},aa=function(e){return e.type===2},At={name:"image",parse:function(e,A){if(A.type===22){var t={url:A.value,type:0};return e.cache.addImage(A.value),t}if(A.type===18){var r=SB[A.name];if(typeof r>"u")throw new Error('Attempting to parse an unsupported image function "'+A.name+'"');return r(e,A.values)}throw new Error("Unsupported image type "+A.type)}};function ia(e){return!(e.type===20&&e.value==="none")&&(e.type!==18||!!SB[e.name])}var SB={"linear-gradient":ta,"-moz-linear-gradient":Le,"-ms-linear-gradient":Le,"-o-linear-gradient":Le,"-webkit-linear-gradient":Le,"radial-gradient":na,"-moz-radial-gradient":De,"-ms-radial-gradient":De,"-o-radial-gradient":De,"-webkit-radial-gradient":De,"-webkit-gradient":Ba},oa={name:"background-image",initialValue:"none",type:1,prefix:!1,parse:function(e,A){if(A.length===0)return[];var t=A[0];return t.type===20&&t.value==="none"?[]:A.filter(function(r){return PA(r)&&ia(r)}).map(function(r){return At.parse(e,r)})}},Qa={name:"background-origin",initialValue:"border-box",prefix:!1,type:1,parse:function(e,A){return A.map(function(t){if(D(t))switch(t.value){case"padding-box":return 1;case"content-box":return 2}return 0})}},ga={name:"background-position",initialValue:"0% 0%",type:1,prefix:!1,parse:function(e,A){return sA(A).map(function(t){return t.filter(M)}).map(hB)}},wa={name:"background-repeat",initialValue:"repeat",prefix:!1,type:1,parse:function(e,A){return sA(A).map(function(t){return t.filter(D).map(function(r){return r.value}).join(" ")}).map(ca)}},ca=function(e){switch(e){case"no-repeat":return 1;case"repeat-x":case"repeat no-repeat":return 2;case"repeat-y":case"no-repeat repeat":return 3;case"repeat":default:return 0}},JA;(function(e){e.AUTO="auto",e.CONTAIN="contain",e.COVER="cover"})(JA||(JA={}));var Ca={name:"background-size",initialValue:"0",prefix:!1,type:1,parse:function(e,A){return sA(A).map(function(t){return t.filter(ua)})}},ua=function(e){return D(e)||M(e)},rr=function(e){return{name:"border-"+e+"-color",initialValue:"transparent",prefix:!1,type:3,format:"color"}},la=rr("top"),fa=rr("right"),Ua=rr("bottom"),Fa=rr("left"),tr=function(e){return{name:"border-radius-"+e,initialValue:"0 0",prefix:!1,type:1,parse:function(A,t){return hB(t.filter(M))}}},ha=tr("top-left"),da=tr("top-right"),Ea=tr("bottom-right"),Ha=tr("bottom-left"),Br=function(e){return{name:"border-"+e+"-style",initialValue:"solid",prefix:!1,type:2,parse:function(A,t){switch(t){case"none":return 0;case"dashed":return 2;case"dotted":return 3;case"double":return 4}return 1}}},pa=Br("top"),Ia=Br("right"),va=Br("bottom"),ya=Br("left"),nr=function(e){return{name:"border-"+e+"-width",initialValue:"0",type:0,prefix:!1,parse:function(A,t){return Ce(t)?t.number:0}}},Ka=nr("top"),ma=nr("right"),La=nr("bottom"),Da=nr("left"),ba={name:"color",initialValue:"transparent",prefix:!1,type:3,format:"color"},xa={name:"direction",initialValue:"ltr",prefix:!1,type:2,parse:function(e,A){switch(A){case"rtl":return 1;case"ltr":default:return 0}}},Ta={name:"display",initialValue:"inline-block",prefix:!1,type:1,parse:function(e,A){return A.filter(D).reduce(function(t,r){return t|Sa(r.value)},0)}},Sa=function(e){switch(e){case"block":case"-webkit-box":return 2;case"inline":return 4;case"run-in":return 8;case"flow":return 16;case"flow-root":return 32;case"table":return 64;case"flex":case"-webkit-flex":return 128;case"grid":case"-ms-grid":return 256;case"ruby":return 512;case"subgrid":return 1024;case"list-item":return 2048;case"table-row-group":return 4096;case"table-header-group":return 8192;case"table-footer-group":return 16384;case"table-row":return 32768;case"table-cell":return 65536;case"table-column-group":return 131072;case"table-column":return 262144;case"table-caption":return 524288;case"ruby-base":return 1048576;case"ruby-text":return 2097152;case"ruby-base-container":return 4194304;case"ruby-text-container":return 8388608;case"contents":return 16777216;case"inline-block":return 33554432;case"inline-list-item":return 67108864;case"inline-table":return 134217728;case"inline-flex":return 268435456;case"inline-grid":return 536870912}return 0},Oa={name:"float",initialValue:"none",prefix:!1,type:2,parse:function(e,A){switch(A){case"left":return 1;case"right":return 2;case"inline-start":return 3;case"inline-end":return 4}return 0}},Ma={name:"letter-spacing",initialValue:"0",prefix:!1,type:0,parse:function(e,A){return A.type===20&&A.value==="normal"?0:A.type===17||A.type===15?A.number:0}},Je;(function(e){e.NORMAL="normal",e.STRICT="strict"})(Je||(Je={}));var Ga={name:"line-break",initialValue:"normal",prefix:!1,type:2,parse:function(e,A){switch(A){case"strict":return Je.STRICT;case"normal":default:return Je.NORMAL}}},Ra={name:"line-height",initialValue:"normal",prefix:!1,type:4},yt=function(e,A){return D(e)&&e.value==="normal"?1.2*A:e.type===17?A*e.number:M(e)?b(e,A):A},Va={name:"list-style-image",initialValue:"none",type:0,prefix:!1,parse:function(e,A){return A.type===20&&A.value==="none"?null:At.parse(e,A)}},Na={name:"list-style-position",initialValue:"outside",prefix:!1,type:2,parse:function(e,A){switch(A){case"inside":return 0;case"outside":default:return 1}}},_r={name:"list-style-type",initialValue:"none",prefix:!1,type:2,parse:function(e,A){switch(A){case"disc":return 0;case"circle":return 1;case"square":return 2;case"decimal":return 3;case"cjk-decimal":return 4;case"decimal-leading-zero":return 5;case"lower-roman":return 6;case"upper-roman":return 7;case"lower-greek":return 8;case"lower-alpha":return 9;case"upper-alpha":return 10;case"arabic-indic":return 11;case"armenian":return 12;case"bengali":return 13;case"cambodian":return 14;case"cjk-earthly-branch":return 15;case"cjk-heavenly-stem":return 16;case"cjk-ideographic":return 17;case"devanagari":return 18;case"ethiopic-numeric":return 19;case"georgian":return 20;case"gujarati":return 21;case"gurmukhi":return 22;case"hebrew":return 22;case"hiragana":return 23;case"hiragana-iroha":return 24;case"japanese-formal":return 25;case"japanese-informal":return 26;case"kannada":return 27;case"katakana":return 28;case"katakana-iroha":return 29;case"khmer":return 30;case"korean-hangul-formal":return 31;case"korean-hanja-formal":return 32;case"korean-hanja-informal":return 33;case"lao":return 34;case"lower-armenian":return 35;case"malayalam":return 36;case"mongolian":return 37;case"myanmar":return 38;case"oriya":return 39;case"persian":return 40;case"simp-chinese-formal":return 41;case"simp-chinese-informal":return 42;case"tamil":return 43;case"telugu":return 44;case"thai":return 45;case"tibetan":return 46;case"trad-chinese-formal":return 47;case"trad-chinese-informal":return 48;case"upper-armenian":return 49;case"disclosure-open":return 50;case"disclosure-closed":return 51;case"none":default:return-1}}},sr=function(e){return{name:"margin-"+e,initialValue:"0",prefix:!1,type:4}},Xa=sr("top"),_a=sr("right"),Ja=sr("bottom"),Pa=sr("left"),ka={name:"overflow",initialValue:"visible",prefix:!1,type:1,parse:function(e,A){return A.filter(D).map(function(t){switch(t.value){case"hidden":return 1;case"scroll":return 2;case"clip":return 3;case"auto":return 4;case"visible":default:return 0}})}},Ya={name:"overflow-wrap",initialValue:"normal",prefix:!1,type:2,parse:function(e,A){switch(A){case"break-word":return"break-word";case"normal":default:return"normal"}}},ar=function(e){return{name:"padding-"+e,initialValue:"0",prefix:!1,type:3,format:"length-percentage"}},Wa=ar("top"),Za=ar("right"),qa=ar("bottom"),ja=ar("left"),za={name:"text-align",initialValue:"left",prefix:!1,type:2,parse:function(e,A){switch(A){case"right":return 2;case"center":case"justify":return 1;case"left":default:return 0}}},$a={name:"position",initialValue:"static",prefix:!1,type:2,parse:function(e,A){switch(A){case"relative":return 1;case"absolute":return 2;case"fixed":return 3;case"sticky":return 4}return 0}},Ai={name:"text-shadow",initialValue:"none",type:1,prefix:!1,parse:function(e,A){return A.length===1&&Xr(A[0],"none")?[]:sA(A).map(function(t){for(var r={color:wA.TRANSPARENT,offsetX:X,offsetY:X,blur:X},B=0,n=0;n"u")throw new Error('Attempting to parse an unsupported transform function "'+A.name+'"');return t(A.values)}return null}},ti=function(e){var A=e.filter(function(t){return t.type===17}).map(function(t){return t.number});return A.length===6?A:null},Bi=function(e){var A=e.filter(function(a){return a.type===17}).map(function(a){return a.number}),t=A[0],r=A[1];A[2],A[3];var B=A[4],n=A[5];A[6],A[7],A[8],A[9],A[10],A[11];var s=A[12],i=A[13];return A[14],A[15],A.length===16?[t,r,B,n,s,i]:null},ni={matrix:ti,matrix3d:Bi},Kt={type:16,number:50,flags:ce},si=[Kt,Kt],ai={name:"transform-origin",initialValue:"50% 50%",prefix:!0,type:1,parse:function(e,A){var t=A.filter(M);return t.length!==2?si:[t[0],t[1]]}},ii={name:"visible",initialValue:"none",prefix:!1,type:2,parse:function(e,A){switch(A){case"hidden":return 1;case"collapse":return 2;case"visible":default:return 0}}},ae;(function(e){e.NORMAL="normal",e.BREAK_ALL="break-all",e.KEEP_ALL="keep-all"})(ae||(ae={}));var oi={name:"word-break",initialValue:"normal",prefix:!1,type:2,parse:function(e,A){switch(A){case"break-all":return ae.BREAK_ALL;case"keep-all":return ae.KEEP_ALL;case"normal":default:return ae.NORMAL}}},Qi={name:"z-index",initialValue:"auto",prefix:!1,type:0,parse:function(e,A){if(A.type===20)return{auto:!0,order:0};if(kA(A))return{auto:!1,order:A.number};throw new Error("Invalid z-index number parsed")}},OB={name:"time",parse:function(e,A){if(A.type===15)switch(A.unit.toLowerCase()){case"s":return 1e3*A.number;case"ms":return A.number}throw new Error("Unsupported time type")}},gi={name:"opacity",initialValue:"1",type:0,prefix:!1,parse:function(e,A){return kA(A)?A.number:1}},wi={name:"text-decoration-color",initialValue:"transparent",prefix:!1,type:3,format:"color"},ci={name:"text-decoration-line",initialValue:"none",prefix:!1,type:1,parse:function(e,A){return A.filter(D).map(function(t){switch(t.value){case"underline":return 1;case"overline":return 2;case"line-through":return 3;case"none":return 4}return 0}).filter(function(t){return t!==0})}},Ci={name:"font-family",initialValue:"",prefix:!1,type:1,parse:function(e,A){var t=[],r=[];return A.forEach(function(B){switch(B.type){case 20:case 0:t.push(B.value);break;case 17:t.push(B.number.toString());break;case 4:r.push(t.join(" ")),t.length=0;break}}),t.length&&r.push(t.join(" ")),r.map(function(B){return B.indexOf(" ")===-1?B:"'"+B+"'"})}},ui={name:"font-size",initialValue:"0",prefix:!1,type:3,format:"length"},li={name:"font-weight",initialValue:"normal",type:0,prefix:!1,parse:function(e,A){if(kA(A))return A.number;if(D(A))switch(A.value){case"bold":return 700;case"normal":default:return 400}return 400}},fi={name:"font-variant",initialValue:"none",type:1,prefix:!1,parse:function(e,A){return A.filter(D).map(function(t){return t.value})}},Ui={name:"font-style",initialValue:"normal",prefix:!1,type:2,parse:function(e,A){switch(A){case"oblique":return"oblique";case"italic":return"italic";case"normal":default:return"normal"}}},G=function(e,A){return(e&A)!==0},Fi={name:"content",initialValue:"none",type:1,prefix:!1,parse:function(e,A){if(A.length===0)return[];var t=A[0];return t.type===20&&t.value==="none"?[]:A}},hi={name:"counter-increment",initialValue:"none",prefix:!0,type:1,parse:function(e,A){if(A.length===0)return null;var t=A[0];if(t.type===20&&t.value==="none")return null;for(var r=[],B=A.filter(FB),n=0;n1?1:0],this.overflowWrap=U(A,Ya,t.overflowWrap),this.paddingTop=U(A,Wa,t.paddingTop),this.paddingRight=U(A,Za,t.paddingRight),this.paddingBottom=U(A,qa,t.paddingBottom),this.paddingLeft=U(A,ja,t.paddingLeft),this.paintOrder=U(A,Ii,t.paintOrder),this.position=U(A,$a,t.position),this.textAlign=U(A,za,t.textAlign),this.textDecorationColor=U(A,wi,(r=t.textDecorationColor)!==null&&r!==void 0?r:t.color),this.textDecorationLine=U(A,ci,(B=t.textDecorationLine)!==null&&B!==void 0?B:t.textDecoration),this.textShadow=U(A,Ai,t.textShadow),this.textTransform=U(A,ei,t.textTransform),this.transform=U(A,ri,t.transform),this.transformOrigin=U(A,ai,t.transformOrigin),this.visibility=U(A,ii,t.visibility),this.webkitTextStrokeColor=U(A,vi,t.webkitTextStrokeColor),this.webkitTextStrokeWidth=U(A,yi,t.webkitTextStrokeWidth),this.wordBreak=U(A,oi,t.wordBreak),this.zIndex=U(A,Qi,t.zIndex)}return e.prototype.isVisible=function(){return this.display>0&&this.opacity>0&&this.visibility===0},e.prototype.isTransparent=function(){return HA(this.backgroundColor)},e.prototype.isTransformed=function(){return this.transform!==null},e.prototype.isPositioned=function(){return this.position!==0},e.prototype.isPositionedWithZIndex=function(){return this.isPositioned()&&!this.zIndex.auto},e.prototype.isFloating=function(){return this.float!==0},e.prototype.isInlineLevel=function(){return G(this.display,4)||G(this.display,33554432)||G(this.display,268435456)||G(this.display,536870912)||G(this.display,67108864)||G(this.display,134217728)},e})(),mi=(function(){function e(A,t){this.content=U(A,Fi,t.content),this.quotes=U(A,Hi,t.quotes)}return e})(),Lt=(function(){function e(A,t){this.counterIncrement=U(A,hi,t.counterIncrement),this.counterReset=U(A,di,t.counterReset)}return e})(),U=function(e,A,t){var r=new fB,B=t!==null&&typeof t<"u"?t.toString():A.initialValue;r.write(B);var n=new UB(r.read());switch(A.type){case 2:var s=n.parseComponentValue();return A.parse(e,D(s)?s.value:A.initialValue);case 0:return A.parse(e,n.parseComponentValue());case 1:return A.parse(e,n.parseComponentValues());case 4:return n.parseComponentValue();case 3:switch(A.format){case"angle":return Ar.parse(e,n.parseComponentValue());case"color":return EA.parse(e,n.parseComponentValue());case"image":return At.parse(e,n.parseComponentValue());case"length":var i=n.parseComponentValue();return pA(i)?i:X;case"length-percentage":var a=n.parseComponentValue();return M(a)?a:X;case"time":return OB.parse(e,n.parseComponentValue())}break}},Li="data-html2canvas-debug",Di=function(e){var A=e.getAttribute(Li);switch(A){case"all":return 1;case"clone":return 2;case"parse":return 3;case"render":return 4;default:return 0}},Jr=function(e,A){var t=Di(e);return t===1||A===t},aA=(function(){function e(A,t){if(this.context=A,this.textNodes=[],this.elements=[],this.flags=0,Jr(t,3))debugger;this.styles=new Ki(A,window.getComputedStyle(t,null)),Yr(t)&&(this.styles.animationDuration.some(function(r){return r>0})&&(t.style.animationDuration="0s"),this.styles.transform!==null&&(t.style.transform="none")),this.bounds=ze(this.context,t),Jr(t,4)&&(this.flags|=16)}return e})(),bi="AAAAAAAAAAAAEA4AGBkAAFAaAAACAAAAAAAIABAAGAAwADgACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAQABIAEQATAAIABAACAAQAAgAEAAIABAAVABcAAgAEAAIABAACAAQAGAAaABwAHgAgACIAI4AlgAIABAAmwCjAKgAsAC2AL4AvQDFAMoA0gBPAVYBWgEIAAgACACMANoAYgFkAWwBdAF8AX0BhQGNAZUBlgGeAaMBlQGWAasBswF8AbsBwwF0AcsBYwHTAQgA2wG/AOMBdAF8AekB8QF0AfkB+wHiAHQBfAEIAAMC5gQIAAsCEgIIAAgAFgIeAggAIgIpAggAMQI5AkACygEIAAgASAJQAlgCYAIIAAgACAAKBQoFCgUTBRMFGQUrBSsFCAAIAAgACAAIAAgACAAIAAgACABdAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABoAmgCrwGvAQgAbgJ2AggAHgEIAAgACADnAXsCCAAIAAgAgwIIAAgACAAIAAgACACKAggAkQKZAggAPADJAAgAoQKkAqwCsgK6AsICCADJAggA0AIIAAgACAAIANYC3gIIAAgACAAIAAgACABAAOYCCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAkASoB+QIEAAgACAA8AEMCCABCBQgACABJBVAFCAAIAAgACAAIAAgACAAIAAgACABTBVoFCAAIAFoFCABfBWUFCAAIAAgACAAIAAgAbQUIAAgACAAIAAgACABzBXsFfQWFBYoFigWKBZEFigWKBYoFmAWfBaYFrgWxBbkFCAAIAAgACAAIAAgACAAIAAgACAAIAMEFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAMgFCADQBQgACAAIAAgACAAIAAgACAAIAAgACAAIAO4CCAAIAAgAiQAIAAgACABAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAD0AggACAD8AggACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIANYFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAMDvwAIAAgAJAIIAAgACAAIAAgACAAIAAgACwMTAwgACAB9BOsEGwMjAwgAKwMyAwsFYgE3A/MEPwMIAEUDTQNRAwgAWQOsAGEDCAAIAAgACAAIAAgACABpAzQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFIQUoBSwFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABtAwgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABMAEwACAAIAAgACAAIABgACAAIAAgACAC/AAgACAAyAQgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACAAIAAwAAgACAAIAAgACAAIAAgACAAIAAAARABIAAgACAAIABQASAAIAAgAIABwAEAAjgCIABsAqAC2AL0AigDQAtwC+IJIQqVAZUBWQqVAZUBlQGVAZUBlQGrC5UBlQGVAZUBlQGVAZUBlQGVAXsKlQGVAbAK6wsrDGUMpQzlDJUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAfAKAAuZA64AtwCJALoC6ADwAAgAuACgA/oEpgO6AqsD+AAIAAgAswMIAAgACAAIAIkAuwP5AfsBwwPLAwgACAAIAAgACADRA9kDCAAIAOED6QMIAAgACAAIAAgACADuA/YDCAAIAP4DyQAIAAgABgQIAAgAXQAOBAgACAAIAAgACAAIABMECAAIAAgACAAIAAgACAD8AAQBCAAIAAgAGgQiBCoECAExBAgAEAEIAAgACAAIAAgACAAIAAgACAAIAAgACAA4BAgACABABEYECAAIAAgATAQYAQgAVAQIAAgACAAIAAgACAAIAAgACAAIAFoECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAOQEIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAB+BAcACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAEABhgSMBAgACAAIAAgAlAQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAwAEAAQABAADAAMAAwADAAQABAAEAAQABAAEAAQABHATAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAdQMIAAgACAAIAAgACAAIAMkACAAIAAgAfQMIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACFA4kDCAAIAAgACAAIAOcBCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAIcDCAAIAAgACAAIAAgACAAIAAgACAAIAJEDCAAIAAgACADFAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABgBAgAZgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAbAQCBXIECAAIAHkECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABAAJwEQACjBKoEsgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAC6BMIECAAIAAgACAAIAAgACABmBAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAxwQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAGYECAAIAAgAzgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBd0FXwUIAOIF6gXxBYoF3gT5BQAGCAaKBYoFigWKBYoFigWKBYoFigWKBYoFigXWBIoFigWKBYoFigWKBYoFigWKBYsFEAaKBYoFigWKBYoFigWKBRQGCACKBYoFigWKBQgACAAIANEECAAIABgGigUgBggAJgYIAC4GMwaKBYoF0wQ3Bj4GigWKBYoFigWKBYoFigWKBYoFigWKBYoFigUIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWLBf///////wQABAAEAAQABAAEAAQABAAEAAQAAwAEAAQAAgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAQADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUAAAAFAAUAAAAFAAUAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAQAAAAUABQAFAAUABQAFAAAAAAAFAAUAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAUAAQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAAABwAHAAcAAAAHAAcABwAFAAEAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAcABwAFAAUAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQABAAAAAAAAAAAAAAAFAAUABQAFAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAHAAcAAAAHAAcAAAAAAAUABQAHAAUAAQAHAAEABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwABAAUABQAFAAUAAAAAAAAAAAAAAAEAAQABAAEAAQABAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABQANAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAABQAHAAUABQAFAAAAAAAAAAcABQAFAAUABQAFAAQABAAEAAQABAAEAAQABAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUAAAAFAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAUAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAcABwAFAAcABwAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUABwAHAAUABQAFAAUAAAAAAAcABwAAAAAABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAAAAAAAAAAABQAFAAAAAAAFAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAFAAUABQAFAAUAAAAFAAUABwAAAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABwAFAAUABQAFAAAAAAAHAAcAAAAAAAcABwAFAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAAAAAAAAAHAAcABwAAAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAUABQAFAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAHAAcABQAHAAcAAAAFAAcABwAAAAcABwAFAAUAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAFAAcABwAFAAUABQAAAAUAAAAHAAcABwAHAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAHAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAUAAAAFAAUAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABwAFAAUABQAFAAUABQAAAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABQAFAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAFAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAHAAUABQAFAAUABQAFAAUABwAHAAcABwAHAAcABwAHAAUABwAHAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABwAHAAcABwAFAAUABwAHAAcAAAAAAAAAAAAHAAcABQAHAAcABwAHAAcABwAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAUABQAFAAUABQAFAAUAAAAFAAAABQAAAAAABQAFAAUABQAFAAUABQAFAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAUABQAFAAUABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABwAFAAcABwAHAAcABwAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAUABQAFAAUABwAHAAUABQAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABQAFAAcABwAHAAUABwAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAcABQAFAAUABQAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAAAAAABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAUABQAHAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAFAAUABQAFAAcABwAFAAUABwAHAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAcABwAFAAUABwAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABQAAAAAABQAFAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAcABwAAAAAAAAAAAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAcABwAFAAcABwAAAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAFAAUABQAAAAUABQAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABwAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAHAAcABQAHAAUABQAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAAABwAHAAAAAAAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAFAAUABwAFAAcABwAFAAcABQAFAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAAAAAABwAHAAcABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAFAAcABwAFAAUABQAFAAUABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAUABQAFAAcABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABQAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAAAAAAFAAUABwAHAAcABwAFAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAHAAUABQAFAAUABQAFAAUABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAABQAAAAUABQAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAHAAcAAAAFAAUAAAAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABQAFAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAABQAFAAUABQAFAAUABQAAAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAFAAUABQAFAAUADgAOAA4ADgAOAA4ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAMAAwADAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAAAAAAAAAAAAsADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwACwAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAADgAOAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAAAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4AAAAOAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAAAAAAAAAAAA4AAAAOAAAAAAAAAAAADgAOAA4AAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAA=",Dt="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",te=typeof Uint8Array>"u"?[]:new Uint8Array(256);for(var be=0;be>4,Q[B++]=(s&15)<<4|i>>2,Q[B++]=(i&3)<<6|a&63;return o},Ti=function(e){for(var A=e.length,t=[],r=0;r>xA,Mi=1<>xA,Ri=MB+Gi,Vi=Ri,Ni=32,Xi=Vi+Ni,_i=65536>>et,Ji=1<=0){if(A<55296||A>56319&&A<=65535)return t=this.index[A>>xA],t=(t<>xA)],t=(t<>et),t=this.index[t],t+=A>>xA&Pi,t=this.index[t],t=(t<"u"?[]:new Uint8Array(256);for(var xe=0;xe=55296&&B<=56319&&t>10)+55296,s%1024+56320)),(B+1===t||r.length>16384)&&(n+=String.fromCharCode.apply(String,r),r.length=0)}return n},Ao=Yi(bi),z="×",Hr="÷",eo=function(e){return Ao.get(e)},ro=function(e,A,t){var r=t-2,B=A[r],n=A[t-1],s=A[t];if(n===Ur&&s===Fr)return z;if(n===Ur||n===Fr||n===Tt||s===Ur||s===Fr||s===Tt)return Hr;if(n===Ot&&[Ot,hr,Mt,Gt].indexOf(s)!==-1||(n===Mt||n===hr)&&(s===hr||s===dr)||(n===Gt||n===dr)&&s===dr||s===Rt||s===St||s===ji||n===qi)return z;if(n===Rt&&s===Vt){for(;B===St;)B=A[--r];if(B===Vt)return z}if(n===Er&&s===Er){for(var i=0;B===Er;)i++,B=A[--r];if(i%2===0)return z}return Hr},to=function(e){var A=zi(e),t=A.length,r=0,B=0,n=A.map(eo);return{next:function(){if(r>=t)return{done:!0,value:null};for(var s=z;rs.x||Q.y>s.y;return s=Q,o===0?!0:g});return e.body.removeChild(A),i},ao=function(){return typeof new Image().crossOrigin<"u"},io=function(){return typeof new XMLHttpRequest().responseType=="string"},oo=function(e){var A=new Image,t=e.createElement("canvas"),r=t.getContext("2d");if(!r)return!1;A.src="data:image/svg+xml,";try{r.drawImage(A,0,0),t.toDataURL()}catch{return!1}return!0},Nt=function(e){return e[0]===0&&e[1]===255&&e[2]===0&&e[3]===255},Qo=function(e){var A=e.createElement("canvas"),t=100;A.width=t,A.height=t;var r=A.getContext("2d");if(!r)return Promise.reject(!1);r.fillStyle="rgb(0, 255, 0)",r.fillRect(0,0,t,t);var B=new Image,n=A.toDataURL();B.src=n;var s=Pr(t,t,0,0,B);return r.fillStyle="red",r.fillRect(0,0,t,t),Xt(s).then(function(i){r.drawImage(i,0,0);var a=r.getImageData(0,0,t,t).data;r.fillStyle="red",r.fillRect(0,0,t,t);var o=e.createElement("div");return o.style.backgroundImage="url("+n+")",o.style.height=t+"px",Nt(a)?Xt(Pr(t,t,0,0,o)):Promise.reject(!1)}).then(function(i){return r.drawImage(i,0,0),Nt(r.getImageData(0,0,t,t).data)}).catch(function(){return!1})},Pr=function(e,A,t,r,B){var n="http://www.w3.org/2000/svg",s=document.createElementNS(n,"svg"),i=document.createElementNS(n,"foreignObject");return s.setAttributeNS(null,"width",e.toString()),s.setAttributeNS(null,"height",A.toString()),i.setAttributeNS(null,"width","100%"),i.setAttributeNS(null,"height","100%"),i.setAttributeNS(null,"x",t.toString()),i.setAttributeNS(null,"y",r.toString()),i.setAttributeNS(null,"externalResourcesRequired","true"),s.appendChild(i),i.appendChild(B),s},Xt=function(e){return new Promise(function(A,t){var r=new Image;r.onload=function(){return A(r)},r.onerror=t,r.src="data:image/svg+xml;charset=utf-8,"+encodeURIComponent(new XMLSerializer().serializeToString(e))})},N={get SUPPORT_RANGE_BOUNDS(){var e=no(document);return Object.defineProperty(N,"SUPPORT_RANGE_BOUNDS",{value:e}),e},get SUPPORT_WORD_BREAKING(){var e=N.SUPPORT_RANGE_BOUNDS&&so(document);return Object.defineProperty(N,"SUPPORT_WORD_BREAKING",{value:e}),e},get SUPPORT_SVG_DRAWING(){var e=oo(document);return Object.defineProperty(N,"SUPPORT_SVG_DRAWING",{value:e}),e},get SUPPORT_FOREIGNOBJECT_DRAWING(){var e=typeof Array.from=="function"&&typeof window.fetch=="function"?Qo(document):Promise.resolve(!1);return Object.defineProperty(N,"SUPPORT_FOREIGNOBJECT_DRAWING",{value:e}),e},get SUPPORT_CORS_IMAGES(){var e=ao();return Object.defineProperty(N,"SUPPORT_CORS_IMAGES",{value:e}),e},get SUPPORT_RESPONSE_TYPE(){var e=io();return Object.defineProperty(N,"SUPPORT_RESPONSE_TYPE",{value:e}),e},get SUPPORT_CORS_XHR(){var e="withCredentials"in new XMLHttpRequest;return Object.defineProperty(N,"SUPPORT_CORS_XHR",{value:e}),e},get SUPPORT_NATIVE_TEXT_SEGMENTATION(){var e=!!(typeof Intl<"u"&&Intl.Segmenter);return Object.defineProperty(N,"SUPPORT_NATIVE_TEXT_SEGMENTATION",{value:e}),e}},ie=(function(){function e(A,t){this.text=A,this.bounds=t}return e})(),go=function(e,A,t,r){var B=Co(A,t),n=[],s=0;return B.forEach(function(i){if(t.textDecorationLine.length||i.trim().length>0)if(N.SUPPORT_RANGE_BOUNDS){var a=_t(r,s,i.length).getClientRects();if(a.length>1){var o=rt(i),Q=0;o.forEach(function(w){n.push(new ie(w,cA.fromDOMRectList(e,_t(r,Q+s,w.length).getClientRects()))),Q+=w.length})}else n.push(new ie(i,cA.fromDOMRectList(e,a)))}else{var g=r.splitText(i.length);n.push(new ie(i,wo(e,r))),r=g}else N.SUPPORT_RANGE_BOUNDS||(r=r.splitText(i.length));s+=i.length}),n},wo=function(e,A){var t=A.ownerDocument;if(t){var r=t.createElement("html2canvaswrapper");r.appendChild(A.cloneNode(!0));var B=A.parentNode;if(B){B.replaceChild(r,A);var n=ze(e,r);return r.firstChild&&B.replaceChild(r.firstChild,r),n}}return cA.EMPTY},_t=function(e,A,t){var r=e.ownerDocument;if(!r)throw new Error("Node has no owner document");var B=r.createRange();return B.setStart(e,A),B.setEnd(e,A+t),B},rt=function(e){if(N.SUPPORT_NATIVE_TEXT_SEGMENTATION){var A=new Intl.Segmenter(void 0,{granularity:"grapheme"});return Array.from(A.segment(e)).map(function(t){return t.segment})}return Bo(e)},co=function(e,A){if(N.SUPPORT_NATIVE_TEXT_SEGMENTATION){var t=new Intl.Segmenter(void 0,{granularity:"word"});return Array.from(t.segment(e)).map(function(r){return r.segment})}return lo(e,A)},Co=function(e,A){return A.letterSpacing!==0?rt(e):co(e,A)},uo=[32,160,4961,65792,65793,4153,4241],lo=function(e,A){for(var t=_n(e,{lineBreak:A.lineBreak,wordBreak:A.overflowWrap==="break-word"?"break-word":A.wordBreak}),r=[],B,n=function(){if(B.value){var s=B.value.slice(),i=$e(s),a="";i.forEach(function(o){uo.indexOf(o)===-1?a+=S(o):(a.length&&r.push(a),r.push(S(o)),a="")}),a.length&&r.push(a)}};!(B=t.next()).done;)n();return r},fo=(function(){function e(A,t,r){this.text=Uo(t.data,r.textTransform),this.textBounds=go(A,this.text,r,t)}return e})(),Uo=function(e,A){switch(A){case 1:return e.toLowerCase();case 3:return e.replace(Fo,ho);case 2:return e.toUpperCase();default:return e}},Fo=/(^|\s|:|-|\(|\))([a-z])/g,ho=function(e,A,t){return e.length>0?A+t.toUpperCase():e},GB=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this;return B.src=r.currentSrc||r.src,B.intrinsicWidth=r.naturalWidth,B.intrinsicHeight=r.naturalHeight,B.context.cache.addImage(B.src),B}return A})(aA),RB=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this;return B.canvas=r,B.intrinsicWidth=r.width,B.intrinsicHeight=r.height,B}return A})(aA),VB=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this,n=new XMLSerializer,s=ze(t,r);return r.setAttribute("width",s.width+"px"),r.setAttribute("height",s.height+"px"),B.svg="data:image/svg+xml,"+encodeURIComponent(n.serializeToString(r)),B.intrinsicWidth=r.width.baseVal.value,B.intrinsicHeight=r.height.baseVal.value,B.context.cache.addImage(B.svg),B}return A})(aA),NB=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this;return B.value=r.value,B}return A})(aA),kr=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this;return B.start=r.start,B.reversed=typeof r.reversed=="boolean"&&r.reversed===!0,B}return A})(aA),Eo=[{type:15,flags:0,unit:"px",number:3}],Ho=[{type:16,flags:0,number:50}],po=function(e){return e.width>e.height?new cA(e.left+(e.width-e.height)/2,e.top,e.height,e.height):e.width0)t.textNodes.push(new fo(e,B,t.styles));else if(_A(B))if(jB(B)&&B.assignedNodes)B.assignedNodes().forEach(function(i){return Ve(e,i,t,r)});else{var s=PB(e,B);s.styles.isVisible()&&(Ko(B,s,r)?s.flags|=4:mo(s.styles)&&(s.flags|=2),yo.indexOf(B.tagName)!==-1&&(s.flags|=8),t.elements.push(s),B.slot,B.shadowRoot?Ve(e,B.shadowRoot,s,r):!Ye(B)&&!WB(B)&&!We(B)&&Ve(e,B,s,r))}},PB=function(e,A){return Wr(A)?new GB(e,A):ZB(A)?new RB(e,A):WB(A)?new VB(e,A):Lo(A)?new NB(e,A):Do(A)?new kr(e,A):bo(A)?new tt(e,A):We(A)?new XB(e,A):Ye(A)?new _B(e,A):qB(A)?new JB(e,A):new aA(e,A)},kB=function(e,A){var t=PB(e,A);return t.flags|=4,Ve(e,A,t,t),t},Ko=function(e,A,t){return A.styles.isPositionedWithZIndex()||A.styles.opacity<1||A.styles.isTransformed()||Bt(e)&&t.styles.isTransparent()},mo=function(e){return e.isPositioned()||e.isFloating()},YB=function(e){return e.nodeType===Node.TEXT_NODE},_A=function(e){return e.nodeType===Node.ELEMENT_NODE},Yr=function(e){return _A(e)&&typeof e.style<"u"&&!Ne(e)},Ne=function(e){return typeof e.className=="object"},Lo=function(e){return e.tagName==="LI"},Do=function(e){return e.tagName==="OL"},bo=function(e){return e.tagName==="INPUT"},xo=function(e){return e.tagName==="HTML"},WB=function(e){return e.tagName==="svg"},Bt=function(e){return e.tagName==="BODY"},ZB=function(e){return e.tagName==="CANVAS"},Pt=function(e){return e.tagName==="VIDEO"},Wr=function(e){return e.tagName==="IMG"},qB=function(e){return e.tagName==="IFRAME"},kt=function(e){return e.tagName==="STYLE"},To=function(e){return e.tagName==="SCRIPT"},Ye=function(e){return e.tagName==="TEXTAREA"},We=function(e){return e.tagName==="SELECT"},jB=function(e){return e.tagName==="SLOT"},Yt=function(e){return e.tagName.indexOf("-")>0},So=(function(){function e(){this.counters={}}return e.prototype.getCounterValue=function(A){var t=this.counters[A];return t&&t.length?t[t.length-1]:1},e.prototype.getCounterValues=function(A){var t=this.counters[A];return t||[]},e.prototype.pop=function(A){var t=this;A.forEach(function(r){return t.counters[r].pop()})},e.prototype.parse=function(A){var t=this,r=A.counterIncrement,B=A.counterReset,n=!0;r!==null&&r.forEach(function(i){var a=t.counters[i.counter];a&&i.increment!==0&&(n=!1,a.length||a.push(1),a[Math.max(0,a.length-1)]+=i.increment)});var s=[];return n&&B.forEach(function(i){var a=t.counters[i.counter];s.push(i.counter),a||(a=t.counters[i.counter]=[]),a.push(i.reset)}),s},e})(),Wt={integers:[1e3,900,500,400,100,90,50,40,10,9,5,4,1],values:["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]},Zt={integers:[9e3,8e3,7e3,6e3,5e3,4e3,3e3,2e3,1e3,900,800,700,600,500,400,300,200,100,90,80,70,60,50,40,30,20,10,9,8,7,6,5,4,3,2,1],values:["Ք","Փ","Ւ","Ց","Ր","Տ","Վ","Ս","Ռ","Ջ","Պ","Չ","Ո","Շ","Ն","Յ","Մ","Ճ","Ղ","Ձ","Հ","Կ","Ծ","Խ","Լ","Ի","Ժ","Թ","Ը","Է","Զ","Ե","Դ","Գ","Բ","Ա"]},Oo={integers:[1e4,9e3,8e3,7e3,6e3,5e3,4e3,3e3,2e3,1e3,400,300,200,100,90,80,70,60,50,40,30,20,19,18,17,16,15,10,9,8,7,6,5,4,3,2,1],values:["י׳","ט׳","ח׳","ז׳","ו׳","ה׳","ד׳","ג׳","ב׳","א׳","ת","ש","ר","ק","צ","פ","ע","ס","נ","מ","ל","כ","יט","יח","יז","טז","טו","י","ט","ח","ז","ו","ה","ד","ג","ב","א"]},Mo={integers:[1e4,9e3,8e3,7e3,6e3,5e3,4e3,3e3,2e3,1e3,900,800,700,600,500,400,300,200,100,90,80,70,60,50,40,30,20,10,9,8,7,6,5,4,3,2,1],values:["ჵ","ჰ","ჯ","ჴ","ხ","ჭ","წ","ძ","ც","ჩ","შ","ყ","ღ","ქ","ფ","ჳ","ტ","ს","რ","ჟ","პ","ო","ჲ","ნ","მ","ლ","კ","ი","თ","ჱ","ზ","ვ","ე","დ","გ","ბ","ა"]},GA=function(e,A,t,r,B,n){return et?we(e,B,n.length>0):r.integers.reduce(function(s,i,a){for(;e>=i;)e-=i,s+=r.values[a];return s},"")+n},zB=function(e,A,t,r){var B="";do t||e--,B=r(e)+B,e/=A;while(e*A>=A);return B},T=function(e,A,t,r,B){var n=t-A+1;return(e<0?"-":"")+(zB(Math.abs(e),n,r,function(s){return S(Math.floor(s%n)+A)})+B)},mA=function(e,A,t){t===void 0&&(t=". ");var r=A.length;return zB(Math.abs(e),r,!1,function(B){return A[Math.floor(B%r)]})+t},NA=1,lA=2,fA=4,Be=8,gA=function(e,A,t,r,B,n){if(e<-9999||e>9999)return we(e,4,B.length>0);var s=Math.abs(e),i=B;if(s===0)return A[0]+i;for(var a=0;s>0&&a<=4;a++){var o=s%10;o===0&&G(n,NA)&&i!==""?i=A[o]+i:o>1||o===1&&a===0||o===1&&a===1&&G(n,lA)||o===1&&a===1&&G(n,fA)&&e>100||o===1&&a>1&&G(n,Be)?i=A[o]+(a>0?t[a-1]:"")+i:o===1&&a>0&&(i=t[a-1]+i),s=Math.floor(s/10)}return(e<0?r:"")+i},qt="十百千萬",jt="拾佰仟萬",zt="マイナス",pr="마이너스",we=function(e,A,t){var r=t?". ":"",B=t?"、":"",n=t?", ":"",s=t?" ":"";switch(A){case 0:return"•"+s;case 1:return"◦"+s;case 2:return"◾"+s;case 5:var i=T(e,48,57,!0,r);return i.length<4?"0"+i:i;case 4:return mA(e,"〇一二三四五六七八九",B);case 6:return GA(e,1,3999,Wt,3,r).toLowerCase();case 7:return GA(e,1,3999,Wt,3,r);case 8:return T(e,945,969,!1,r);case 9:return T(e,97,122,!1,r);case 10:return T(e,65,90,!1,r);case 11:return T(e,1632,1641,!0,r);case 12:case 49:return GA(e,1,9999,Zt,3,r);case 35:return GA(e,1,9999,Zt,3,r).toLowerCase();case 13:return T(e,2534,2543,!0,r);case 14:case 30:return T(e,6112,6121,!0,r);case 15:return mA(e,"子丑寅卯辰巳午未申酉戌亥",B);case 16:return mA(e,"甲乙丙丁戊己庚辛壬癸",B);case 17:case 48:return gA(e,"零一二三四五六七八九",qt,"負",B,lA|fA|Be);case 47:return gA(e,"零壹貳參肆伍陸柒捌玖",jt,"負",B,NA|lA|fA|Be);case 42:return gA(e,"零一二三四五六七八九",qt,"负",B,lA|fA|Be);case 41:return gA(e,"零壹贰叁肆伍陆柒捌玖",jt,"负",B,NA|lA|fA|Be);case 26:return gA(e,"〇一二三四五六七八九","十百千万",zt,B,0);case 25:return gA(e,"零壱弐参四伍六七八九","拾百千万",zt,B,NA|lA|fA);case 31:return gA(e,"영일이삼사오육칠팔구","십백천만",pr,n,NA|lA|fA);case 33:return gA(e,"零一二三四五六七八九","十百千萬",pr,n,0);case 32:return gA(e,"零壹貳參四五六七八九","拾百千",pr,n,NA|lA|fA);case 18:return T(e,2406,2415,!0,r);case 20:return GA(e,1,19999,Mo,3,r);case 21:return T(e,2790,2799,!0,r);case 22:return T(e,2662,2671,!0,r);case 22:return GA(e,1,10999,Oo,3,r);case 23:return mA(e,"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん");case 24:return mA(e,"いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす");case 27:return T(e,3302,3311,!0,r);case 28:return mA(e,"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン",B);case 29:return mA(e,"イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス",B);case 34:return T(e,3792,3801,!0,r);case 37:return T(e,6160,6169,!0,r);case 38:return T(e,4160,4169,!0,r);case 39:return T(e,2918,2927,!0,r);case 40:return T(e,1776,1785,!0,r);case 43:return T(e,3046,3055,!0,r);case 44:return T(e,3174,3183,!0,r);case 45:return T(e,3664,3673,!0,r);case 46:return T(e,3872,3881,!0,r);case 3:default:return T(e,48,57,!0,r)}},$B="data-html2canvas-ignore",$t=(function(){function e(A,t,r){if(this.context=A,this.options=r,this.scrolledElements=[],this.referenceElement=t,this.counters=new So,this.quoteDepth=0,!t.ownerDocument)throw new Error("Cloned element does not have an owner document");this.documentElement=this.cloneNode(t.ownerDocument.documentElement,!1)}return e.prototype.toIFrame=function(A,t){var r=this,B=Go(A,t);if(!B.contentWindow)return Promise.reject("Unable to find iframe window");var n=A.defaultView.pageXOffset,s=A.defaultView.pageYOffset,i=B.contentWindow,a=i.document,o=No(B).then(function(){return P(r,void 0,void 0,function(){var Q,g;return _(this,function(w){switch(w.label){case 0:return this.scrolledElements.forEach(Po),i&&(i.scrollTo(t.left,t.top),/(iPad|iPhone|iPod)/g.test(navigator.userAgent)&&(i.scrollY!==t.top||i.scrollX!==t.left)&&(this.context.logger.warn("Unable to restore scroll position for cloned document"),this.context.windowBounds=this.context.windowBounds.add(i.scrollX-t.left,i.scrollY-t.top,0,0))),Q=this.options.onclone,g=this.clonedReferenceElement,typeof g>"u"?[2,Promise.reject("Error finding the "+this.referenceElement.nodeName+" in the cloned document")]:a.fonts&&a.fonts.ready?[4,a.fonts.ready]:[3,2];case 1:w.sent(),w.label=2;case 2:return/(AppleWebKit)/g.test(navigator.userAgent)?[4,Vo(a)]:[3,4];case 3:w.sent(),w.label=4;case 4:return typeof Q=="function"?[2,Promise.resolve().then(function(){return Q(a,g)}).then(function(){return B})]:[2,B]}})})});return a.open(),a.write(_o(document.doctype)+""),Jo(this.referenceElement.ownerDocument,n,s),a.replaceChild(a.adoptNode(this.documentElement),a.documentElement),a.close(),o},e.prototype.createElementClone=function(A){if(Jr(A,2))debugger;if(ZB(A))return this.createCanvasClone(A);if(Pt(A))return this.createVideoClone(A);if(kt(A))return this.createStyleClone(A);var t=A.cloneNode(!1);return Wr(t)&&(Wr(A)&&A.currentSrc&&A.currentSrc!==A.src&&(t.src=A.currentSrc,t.srcset=""),t.loading==="lazy"&&(t.loading="eager")),Yt(t)?this.createCustomElementClone(t):t},e.prototype.createCustomElementClone=function(A){var t=document.createElement("html2canvascustomelement");return Ir(A.style,t),t},e.prototype.createStyleClone=function(A){try{var t=A.sheet;if(t&&t.cssRules){var r=[].slice.call(t.cssRules,0).reduce(function(n,s){return s&&typeof s.cssText=="string"?n+s.cssText:n},""),B=A.cloneNode(!1);return B.textContent=r,B}}catch(n){if(this.context.logger.error("Unable to access cssRules property",n),n.name!=="SecurityError")throw n}return A.cloneNode(!1)},e.prototype.createCanvasClone=function(A){var t;if(this.options.inlineImages&&A.ownerDocument){var r=A.ownerDocument.createElement("img");try{return r.src=A.toDataURL(),r}catch{this.context.logger.info("Unable to inline canvas contents, canvas is tainted",A)}}var B=A.cloneNode(!1);try{B.width=A.width,B.height=A.height;var n=A.getContext("2d"),s=B.getContext("2d");if(s)if(!this.options.allowTaint&&n)s.putImageData(n.getImageData(0,0,A.width,A.height),0,0);else{var i=(t=A.getContext("webgl2"))!==null&&t!==void 0?t:A.getContext("webgl");if(i){var a=i.getContextAttributes();a?.preserveDrawingBuffer===!1&&this.context.logger.warn("Unable to clone WebGL context as it has preserveDrawingBuffer=false",A)}s.drawImage(A,0,0)}return B}catch{this.context.logger.info("Unable to clone canvas as it is tainted",A)}return B},e.prototype.createVideoClone=function(A){var t=A.ownerDocument.createElement("canvas");t.width=A.offsetWidth,t.height=A.offsetHeight;var r=t.getContext("2d");try{return r&&(r.drawImage(A,0,0,t.width,t.height),this.options.allowTaint||r.getImageData(0,0,t.width,t.height)),t}catch{this.context.logger.info("Unable to clone video as it is tainted",A)}var B=A.ownerDocument.createElement("canvas");return B.width=A.offsetWidth,B.height=A.offsetHeight,B},e.prototype.appendChildNode=function(A,t,r){(!_A(t)||!To(t)&&!t.hasAttribute($B)&&(typeof this.options.ignoreElements!="function"||!this.options.ignoreElements(t)))&&(!this.options.copyStyles||!_A(t)||!kt(t))&&A.appendChild(this.cloneNode(t,r))},e.prototype.cloneChildNodes=function(A,t,r){for(var B=this,n=A.shadowRoot?A.shadowRoot.firstChild:A.firstChild;n;n=n.nextSibling)if(_A(n)&&jB(n)&&typeof n.assignedNodes=="function"){var s=n.assignedNodes();s.length&&s.forEach(function(i){return B.appendChildNode(t,i,r)})}else this.appendChildNode(t,n,r)},e.prototype.cloneNode=function(A,t){if(YB(A))return document.createTextNode(A.data);if(!A.ownerDocument)return A.cloneNode(!1);var r=A.ownerDocument.defaultView;if(r&&_A(A)&&(Yr(A)||Ne(A))){var B=this.createElementClone(A);B.style.transitionProperty="none";var n=r.getComputedStyle(A),s=r.getComputedStyle(A,":before"),i=r.getComputedStyle(A,":after");this.referenceElement===A&&Yr(B)&&(this.clonedReferenceElement=B),Bt(B)&&Wo(B);var a=this.counters.parse(new Lt(this.context,n)),o=this.resolvePseudoContent(A,B,s,oe.BEFORE);Yt(A)&&(t=!0),Pt(A)||this.cloneChildNodes(A,B,t),o&&B.insertBefore(o,B.firstChild);var Q=this.resolvePseudoContent(A,B,i,oe.AFTER);return Q&&B.appendChild(Q),this.counters.pop(a),(n&&(this.options.copyStyles||Ne(A))&&!qB(A)||t)&&Ir(n,B),(A.scrollTop!==0||A.scrollLeft!==0)&&this.scrolledElements.push([B,A.scrollLeft,A.scrollTop]),(Ye(A)||We(A))&&(Ye(B)||We(B))&&(B.value=A.value),B}return A.cloneNode(!1)},e.prototype.resolvePseudoContent=function(A,t,r,B){var n=this;if(r){var s=r.content,i=t.ownerDocument;if(!(!i||!s||s==="none"||s==="-moz-alt-content"||r.display==="none")){this.counters.parse(new Lt(this.context,r));var a=new mi(this.context,r),o=i.createElement("html2canvaspseudoelement");Ir(r,o),a.content.forEach(function(g){if(g.type===0)o.appendChild(i.createTextNode(g.value));else if(g.type===22){var w=i.createElement("img");w.src=g.value,w.style.opacity="1",o.appendChild(w)}else if(g.type===18){if(g.name==="attr"){var f=g.values.filter(D);f.length&&o.appendChild(i.createTextNode(A.getAttribute(f[0].value)||""))}else if(g.name==="counter"){var c=g.values.filter(PA),C=c[0],H=c[1];if(C&&D(C)){var h=n.counters.getCounterValue(C.value),F=H&&D(H)?_r.parse(n.context,H.value):3;o.appendChild(i.createTextNode(we(h,F,!1)))}}else if(g.name==="counters"){var K=g.values.filter(PA),C=K[0],p=K[1],H=K[2];if(C&&D(C)){var d=n.counters.getCounterValues(C.value),l=H&&D(H)?_r.parse(n.context,H.value):3,v=p&&p.type===0?p.value:"",y=d.map(function(k){return we(k,l,!1)}).join(v);o.appendChild(i.createTextNode(y))}}}else if(g.type===20)switch(g.value){case"open-quote":o.appendChild(i.createTextNode(mt(a.quotes,n.quoteDepth++,!0)));break;case"close-quote":o.appendChild(i.createTextNode(mt(a.quotes,--n.quoteDepth,!1)));break;default:o.appendChild(i.createTextNode(g.value))}}),o.className=Zr+" "+qr;var Q=B===oe.BEFORE?" "+Zr:" "+qr;return Ne(t)?t.className.baseValue+=Q:t.className+=Q,o}}},e.destroy=function(A){return A.parentNode?(A.parentNode.removeChild(A),!0):!1},e})(),oe;(function(e){e[e.BEFORE=0]="BEFORE",e[e.AFTER=1]="AFTER"})(oe||(oe={}));var Go=function(e,A){var t=e.createElement("iframe");return t.className="html2canvas-container",t.style.visibility="hidden",t.style.position="fixed",t.style.left="-10000px",t.style.top="0px",t.style.border="0",t.width=A.width.toString(),t.height=A.height.toString(),t.scrolling="no",t.setAttribute($B,"true"),e.body.appendChild(t),t},Ro=function(e){return new Promise(function(A){if(e.complete){A();return}if(!e.src){A();return}e.onload=A,e.onerror=A})},Vo=function(e){return Promise.all([].slice.call(e.images,0).map(Ro))},No=function(e){return new Promise(function(A,t){var r=e.contentWindow;if(!r)return t("No window assigned for iframe");var B=r.document;r.onload=e.onload=function(){r.onload=e.onload=null;var n=setInterval(function(){B.body.childNodes.length>0&&B.readyState==="complete"&&(clearInterval(n),A(e))},50)}})},Xo=["all","d","content"],Ir=function(e,A){for(var t=e.length-1;t>=0;t--){var r=e.item(t);Xo.indexOf(r)===-1&&A.style.setProperty(r,e.getPropertyValue(r))}return A},_o=function(e){var A="";return e&&(A+=""),A},Jo=function(e,A,t){e&&e.defaultView&&(A!==e.defaultView.pageXOffset||t!==e.defaultView.pageYOffset)&&e.defaultView.scrollTo(A,t)},Po=function(e){var A=e[0],t=e[1],r=e[2];A.scrollLeft=t,A.scrollTop=r},ko=":before",Yo=":after",Zr="___html2canvas___pseudoelement_before",qr="___html2canvas___pseudoelement_after",AB=`{ + content: "" !important; + display: none !important; +}`,Wo=function(e){Zo(e,"."+Zr+ko+AB+` + .`+qr+Yo+AB)},Zo=function(e,A){var t=e.ownerDocument;if(t){var r=t.createElement("style");r.textContent=A,e.appendChild(r)}},An=(function(){function e(){}return e.getOrigin=function(A){var t=e._link;return t?(t.href=A,t.href=t.href,t.protocol+t.hostname+t.port):"about:blank"},e.isSameOrigin=function(A){return e.getOrigin(A)===e._origin},e.setContext=function(A){e._link=A.document.createElement("a"),e._origin=e.getOrigin(A.location.href)},e._origin="about:blank",e})(),qo=(function(){function e(A,t){this.context=A,this._options=t,this._cache={}}return e.prototype.addImage=function(A){var t=Promise.resolve();return this.has(A)||(yr(A)||AQ(A))&&(this._cache[A]=this.loadImage(A)).catch(function(){}),t},e.prototype.match=function(A){return this._cache[A]},e.prototype.loadImage=function(A){return P(this,void 0,void 0,function(){var t,r,B,n,s=this;return _(this,function(i){switch(i.label){case 0:return t=An.isSameOrigin(A),r=!vr(A)&&this._options.useCORS===!0&&N.SUPPORT_CORS_IMAGES&&!t,B=!vr(A)&&!t&&!yr(A)&&typeof this._options.proxy=="string"&&N.SUPPORT_CORS_XHR&&!r,!t&&this._options.allowTaint===!1&&!vr(A)&&!yr(A)&&!B&&!r?[2]:(n=A,B?[4,this.proxy(n)]:[3,2]);case 1:n=i.sent(),i.label=2;case 2:return this.context.logger.debug("Added image "+A.substring(0,256)),[4,new Promise(function(a,o){var Q=new Image;Q.onload=function(){return a(Q)},Q.onerror=o,(eQ(n)||r)&&(Q.crossOrigin="anonymous"),Q.src=n,Q.complete===!0&&setTimeout(function(){return a(Q)},500),s._options.imageTimeout>0&&setTimeout(function(){return o("Timed out ("+s._options.imageTimeout+"ms) loading image")},s._options.imageTimeout)})];case 3:return[2,i.sent()]}})})},e.prototype.has=function(A){return typeof this._cache[A]<"u"},e.prototype.keys=function(){return Promise.resolve(Object.keys(this._cache))},e.prototype.proxy=function(A){var t=this,r=this._options.proxy;if(!r)throw new Error("No proxy defined");var B=A.substring(0,256);return new Promise(function(n,s){var i=N.SUPPORT_RESPONSE_TYPE?"blob":"text",a=new XMLHttpRequest;a.onload=function(){if(a.status===200)if(i==="text")n(a.response);else{var g=new FileReader;g.addEventListener("load",function(){return n(g.result)},!1),g.addEventListener("error",function(w){return s(w)},!1),g.readAsDataURL(a.response)}else s("Failed to proxy resource "+B+" with status code "+a.status)},a.onerror=s;var o=r.indexOf("?")>-1?"&":"?";if(a.open("GET",""+r+o+"url="+encodeURIComponent(A)+"&responseType="+i),i!=="text"&&a instanceof XMLHttpRequest&&(a.responseType=i),t._options.imageTimeout){var Q=t._options.imageTimeout;a.timeout=Q,a.ontimeout=function(){return s("Timed out ("+Q+"ms) proxying "+B)}}a.send()})},e})(),jo=/^data:image\/svg\+xml/i,zo=/^data:image\/.*;base64,/i,$o=/^data:image\/.*/i,AQ=function(e){return N.SUPPORT_SVG_DRAWING||!rQ(e)},vr=function(e){return $o.test(e)},eQ=function(e){return zo.test(e)},yr=function(e){return e.substr(0,4)==="blob"},rQ=function(e){return e.substr(-3).toLowerCase()==="svg"||jo.test(e)},u=(function(){function e(A,t){this.type=0,this.x=A,this.y=t}return e.prototype.add=function(A,t){return new e(this.x+A,this.y+t)},e})(),RA=function(e,A,t){return new u(e.x+(A.x-e.x)*t,e.y+(A.y-e.y)*t)},Te=(function(){function e(A,t,r,B){this.type=1,this.start=A,this.startControl=t,this.endControl=r,this.end=B}return e.prototype.subdivide=function(A,t){var r=RA(this.start,this.startControl,A),B=RA(this.startControl,this.endControl,A),n=RA(this.endControl,this.end,A),s=RA(r,B,A),i=RA(B,n,A),a=RA(s,i,A);return t?new e(this.start,r,s,a):new e(a,i,n,this.end)},e.prototype.add=function(A,t){return new e(this.start.add(A,t),this.startControl.add(A,t),this.endControl.add(A,t),this.end.add(A,t))},e.prototype.reverse=function(){return new e(this.end,this.endControl,this.startControl,this.start)},e})(),$=function(e){return e.type===1},tQ=(function(){function e(A){var t=A.styles,r=A.bounds,B=re(t.borderTopLeftRadius,r.width,r.height),n=B[0],s=B[1],i=re(t.borderTopRightRadius,r.width,r.height),a=i[0],o=i[1],Q=re(t.borderBottomRightRadius,r.width,r.height),g=Q[0],w=Q[1],f=re(t.borderBottomLeftRadius,r.width,r.height),c=f[0],C=f[1],H=[];H.push((n+a)/r.width),H.push((c+g)/r.width),H.push((s+C)/r.height),H.push((o+w)/r.height);var h=Math.max.apply(Math,H);h>1&&(n/=h,s/=h,a/=h,o/=h,g/=h,w/=h,c/=h,C/=h);var F=r.width-a,K=r.height-w,p=r.width-g,d=r.height-C,l=t.borderTopWidth,v=t.borderRightWidth,y=t.borderBottomWidth,E=t.borderLeftWidth,O=b(t.paddingTop,A.bounds.width),k=b(t.paddingRight,A.bounds.width),q=b(t.paddingBottom,A.bounds.width),L=b(t.paddingLeft,A.bounds.width);this.topLeftBorderDoubleOuterBox=n>0||s>0?x(r.left+E/3,r.top+l/3,n-E/3,s-l/3,m.TOP_LEFT):new u(r.left+E/3,r.top+l/3),this.topRightBorderDoubleOuterBox=n>0||s>0?x(r.left+F,r.top+l/3,a-v/3,o-l/3,m.TOP_RIGHT):new u(r.left+r.width-v/3,r.top+l/3),this.bottomRightBorderDoubleOuterBox=g>0||w>0?x(r.left+p,r.top+K,g-v/3,w-y/3,m.BOTTOM_RIGHT):new u(r.left+r.width-v/3,r.top+r.height-y/3),this.bottomLeftBorderDoubleOuterBox=c>0||C>0?x(r.left+E/3,r.top+d,c-E/3,C-y/3,m.BOTTOM_LEFT):new u(r.left+E/3,r.top+r.height-y/3),this.topLeftBorderDoubleInnerBox=n>0||s>0?x(r.left+E*2/3,r.top+l*2/3,n-E*2/3,s-l*2/3,m.TOP_LEFT):new u(r.left+E*2/3,r.top+l*2/3),this.topRightBorderDoubleInnerBox=n>0||s>0?x(r.left+F,r.top+l*2/3,a-v*2/3,o-l*2/3,m.TOP_RIGHT):new u(r.left+r.width-v*2/3,r.top+l*2/3),this.bottomRightBorderDoubleInnerBox=g>0||w>0?x(r.left+p,r.top+K,g-v*2/3,w-y*2/3,m.BOTTOM_RIGHT):new u(r.left+r.width-v*2/3,r.top+r.height-y*2/3),this.bottomLeftBorderDoubleInnerBox=c>0||C>0?x(r.left+E*2/3,r.top+d,c-E*2/3,C-y*2/3,m.BOTTOM_LEFT):new u(r.left+E*2/3,r.top+r.height-y*2/3),this.topLeftBorderStroke=n>0||s>0?x(r.left+E/2,r.top+l/2,n-E/2,s-l/2,m.TOP_LEFT):new u(r.left+E/2,r.top+l/2),this.topRightBorderStroke=n>0||s>0?x(r.left+F,r.top+l/2,a-v/2,o-l/2,m.TOP_RIGHT):new u(r.left+r.width-v/2,r.top+l/2),this.bottomRightBorderStroke=g>0||w>0?x(r.left+p,r.top+K,g-v/2,w-y/2,m.BOTTOM_RIGHT):new u(r.left+r.width-v/2,r.top+r.height-y/2),this.bottomLeftBorderStroke=c>0||C>0?x(r.left+E/2,r.top+d,c-E/2,C-y/2,m.BOTTOM_LEFT):new u(r.left+E/2,r.top+r.height-y/2),this.topLeftBorderBox=n>0||s>0?x(r.left,r.top,n,s,m.TOP_LEFT):new u(r.left,r.top),this.topRightBorderBox=a>0||o>0?x(r.left+F,r.top,a,o,m.TOP_RIGHT):new u(r.left+r.width,r.top),this.bottomRightBorderBox=g>0||w>0?x(r.left+p,r.top+K,g,w,m.BOTTOM_RIGHT):new u(r.left+r.width,r.top+r.height),this.bottomLeftBorderBox=c>0||C>0?x(r.left,r.top+d,c,C,m.BOTTOM_LEFT):new u(r.left,r.top+r.height),this.topLeftPaddingBox=n>0||s>0?x(r.left+E,r.top+l,Math.max(0,n-E),Math.max(0,s-l),m.TOP_LEFT):new u(r.left+E,r.top+l),this.topRightPaddingBox=a>0||o>0?x(r.left+Math.min(F,r.width-v),r.top+l,F>r.width+v?0:Math.max(0,a-v),Math.max(0,o-l),m.TOP_RIGHT):new u(r.left+r.width-v,r.top+l),this.bottomRightPaddingBox=g>0||w>0?x(r.left+Math.min(p,r.width-E),r.top+Math.min(K,r.height-y),Math.max(0,g-v),Math.max(0,w-y),m.BOTTOM_RIGHT):new u(r.left+r.width-v,r.top+r.height-y),this.bottomLeftPaddingBox=c>0||C>0?x(r.left+E,r.top+Math.min(d,r.height-y),Math.max(0,c-E),Math.max(0,C-y),m.BOTTOM_LEFT):new u(r.left+E,r.top+r.height-y),this.topLeftContentBox=n>0||s>0?x(r.left+E+L,r.top+l+O,Math.max(0,n-(E+L)),Math.max(0,s-(l+O)),m.TOP_LEFT):new u(r.left+E+L,r.top+l+O),this.topRightContentBox=a>0||o>0?x(r.left+Math.min(F,r.width+E+L),r.top+l+O,F>r.width+E+L?0:a-E+L,o-(l+O),m.TOP_RIGHT):new u(r.left+r.width-(v+k),r.top+l+O),this.bottomRightContentBox=g>0||w>0?x(r.left+Math.min(p,r.width-(E+L)),r.top+Math.min(K,r.height+l+O),Math.max(0,g-(v+k)),w-(y+q),m.BOTTOM_RIGHT):new u(r.left+r.width-(v+k),r.top+r.height-(y+q)),this.bottomLeftContentBox=c>0||C>0?x(r.left+E+L,r.top+d,Math.max(0,c-(E+L)),C-(y+q),m.BOTTOM_LEFT):new u(r.left+E+L,r.top+r.height-(y+q))}return e})(),m;(function(e){e[e.TOP_LEFT=0]="TOP_LEFT",e[e.TOP_RIGHT=1]="TOP_RIGHT",e[e.BOTTOM_RIGHT=2]="BOTTOM_RIGHT",e[e.BOTTOM_LEFT=3]="BOTTOM_LEFT"})(m||(m={}));var x=function(e,A,t,r,B){var n=4*((Math.sqrt(2)-1)/3),s=t*n,i=r*n,a=e+t,o=A+r;switch(B){case m.TOP_LEFT:return new Te(new u(e,o),new u(e,o-i),new u(a-s,A),new u(a,A));case m.TOP_RIGHT:return new Te(new u(e,A),new u(e+s,A),new u(a,o-i),new u(a,o));case m.BOTTOM_RIGHT:return new Te(new u(a,A),new u(a,A+i),new u(e+s,o),new u(e,o));case m.BOTTOM_LEFT:default:return new Te(new u(a,o),new u(a-s,o),new u(e,A+i),new u(e,A))}},Ze=function(e){return[e.topLeftBorderBox,e.topRightBorderBox,e.bottomRightBorderBox,e.bottomLeftBorderBox]},BQ=function(e){return[e.topLeftContentBox,e.topRightContentBox,e.bottomRightContentBox,e.bottomLeftContentBox]},qe=function(e){return[e.topLeftPaddingBox,e.topRightPaddingBox,e.bottomRightPaddingBox,e.bottomLeftPaddingBox]},nQ=(function(){function e(A,t,r){this.offsetX=A,this.offsetY=t,this.matrix=r,this.type=0,this.target=6}return e})(),Se=(function(){function e(A,t){this.path=A,this.target=t,this.type=1}return e})(),sQ=(function(){function e(A){this.opacity=A,this.type=2,this.target=6}return e})(),aQ=function(e){return e.type===0},en=function(e){return e.type===1},iQ=function(e){return e.type===2},eB=function(e,A){return e.length===A.length?e.some(function(t,r){return t===A[r]}):!1},oQ=function(e,A,t,r,B){return e.map(function(n,s){switch(s){case 0:return n.add(A,t);case 1:return n.add(A+r,t);case 2:return n.add(A+r,t+B);case 3:return n.add(A,t+B)}return n})},rn=(function(){function e(A){this.element=A,this.inlineLevel=[],this.nonInlineLevel=[],this.negativeZIndex=[],this.zeroOrAutoZIndexOrTransformedOrOpacity=[],this.positiveZIndex=[],this.nonPositionedFloats=[],this.nonPositionedInlineLevel=[]}return e})(),tn=(function(){function e(A,t){if(this.container=A,this.parent=t,this.effects=[],this.curves=new tQ(this.container),this.container.styles.opacity<1&&this.effects.push(new sQ(this.container.styles.opacity)),this.container.styles.transform!==null){var r=this.container.bounds.left+this.container.styles.transformOrigin[0].number,B=this.container.bounds.top+this.container.styles.transformOrigin[1].number,n=this.container.styles.transform;this.effects.push(new nQ(r,B,n))}if(this.container.styles.overflowX!==0){var s=Ze(this.curves),i=qe(this.curves);eB(s,i)?this.effects.push(new Se(s,6)):(this.effects.push(new Se(s,2)),this.effects.push(new Se(i,4)))}}return e.prototype.getEffects=function(A){for(var t=[2,3].indexOf(this.container.styles.position)===-1,r=this.parent,B=this.effects.slice(0);r;){var n=r.effects.filter(function(a){return!en(a)});if(t||r.container.styles.position!==0||!r.parent){if(B.unshift.apply(B,n),t=[2,3].indexOf(r.container.styles.position)===-1,r.container.styles.overflowX!==0){var s=Ze(r.curves),i=qe(r.curves);eB(s,i)||B.unshift(new Se(i,6))}}else B.unshift.apply(B,n);r=r.parent}return B.filter(function(a){return G(a.target,A)})},e})(),jr=function(e,A,t,r){e.container.elements.forEach(function(B){var n=G(B.flags,4),s=G(B.flags,2),i=new tn(B,e);G(B.styles.display,2048)&&r.push(i);var a=G(B.flags,8)?[]:r;if(n||s){var o=n||B.styles.isPositioned()?t:A,Q=new rn(i);if(B.styles.isPositioned()||B.styles.opacity<1||B.styles.isTransformed()){var g=B.styles.zIndex.order;if(g<0){var w=0;o.negativeZIndex.some(function(c,C){return g>c.element.container.styles.zIndex.order?(w=C,!1):w>0}),o.negativeZIndex.splice(w,0,Q)}else if(g>0){var f=0;o.positiveZIndex.some(function(c,C){return g>=c.element.container.styles.zIndex.order?(f=C+1,!1):f>0}),o.positiveZIndex.splice(f,0,Q)}else o.zeroOrAutoZIndexOrTransformedOrOpacity.push(Q)}else B.styles.isFloating()?o.nonPositionedFloats.push(Q):o.nonPositionedInlineLevel.push(Q);jr(i,Q,n?Q:t,a)}else B.styles.isInlineLevel()?A.inlineLevel.push(i):A.nonInlineLevel.push(i),jr(i,A,t,a);G(B.flags,8)&&Bn(B,a)})},Bn=function(e,A){for(var t=e instanceof kr?e.start:1,r=e instanceof kr?e.reversed:!1,B=0;B"u"?e[0]:t},fQ=function(e,A,t,r,B){var n=A[0],s=A[1],i=t[0],a=t[1];switch(e){case 2:return[new u(Math.round(r.left),Math.round(r.top+s)),new u(Math.round(r.left+r.width),Math.round(r.top+s)),new u(Math.round(r.left+r.width),Math.round(a+r.top+s)),new u(Math.round(r.left),Math.round(a+r.top+s))];case 3:return[new u(Math.round(r.left+n),Math.round(r.top)),new u(Math.round(r.left+n+i),Math.round(r.top)),new u(Math.round(r.left+n+i),Math.round(r.height+r.top)),new u(Math.round(r.left+n),Math.round(r.height+r.top))];case 1:return[new u(Math.round(r.left+n),Math.round(r.top+s)),new u(Math.round(r.left+n+i),Math.round(r.top+s)),new u(Math.round(r.left+n+i),Math.round(r.top+s+a)),new u(Math.round(r.left+n),Math.round(r.top+s+a))];default:return[new u(Math.round(B.left),Math.round(B.top)),new u(Math.round(B.left+B.width),Math.round(B.top)),new u(Math.round(B.left+B.width),Math.round(B.height+B.top)),new u(Math.round(B.left),Math.round(B.height+B.top))]}},UQ="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",tB="Hidden Text",FQ=(function(){function e(A){this._data={},this._document=A}return e.prototype.parseMetrics=function(A,t){var r=this._document.createElement("div"),B=this._document.createElement("img"),n=this._document.createElement("span"),s=this._document.body;r.style.visibility="hidden",r.style.fontFamily=A,r.style.fontSize=t,r.style.margin="0",r.style.padding="0",r.style.whiteSpace="nowrap",s.appendChild(r),B.src=UQ,B.width=1,B.height=1,B.style.margin="0",B.style.padding="0",B.style.verticalAlign="baseline",n.style.fontFamily=A,n.style.fontSize=t,n.style.margin="0",n.style.padding="0",n.appendChild(this._document.createTextNode(tB)),r.appendChild(n),r.appendChild(B);var i=B.offsetTop-n.offsetTop+2;r.removeChild(n),r.appendChild(this._document.createTextNode(tB)),r.style.lineHeight="normal",B.style.verticalAlign="super";var a=B.offsetTop-r.offsetTop+2;return s.removeChild(r),{baseline:i,middle:a}},e.prototype.getMetrics=function(A,t){var r=A+" "+t;return typeof this._data[r]>"u"&&(this._data[r]=this.parseMetrics(A,t)),this._data[r]},e})(),sn=(function(){function e(A,t){this.context=A,this.options=t}return e})(),hQ=1e4,dQ=(function(e){tA(A,e);function A(t,r){var B=e.call(this,t,r)||this;return B._activeEffects=[],B.canvas=r.canvas?r.canvas:document.createElement("canvas"),B.ctx=B.canvas.getContext("2d"),r.canvas||(B.canvas.width=Math.floor(r.width*r.scale),B.canvas.height=Math.floor(r.height*r.scale),B.canvas.style.width=r.width+"px",B.canvas.style.height=r.height+"px"),B.fontMetrics=new FQ(document),B.ctx.scale(B.options.scale,B.options.scale),B.ctx.translate(-r.x,-r.y),B.ctx.textBaseline="bottom",B._activeEffects=[],B.context.logger.debug("Canvas renderer initialized ("+r.width+"x"+r.height+") with scale "+r.scale),B}return A.prototype.applyEffects=function(t){for(var r=this;this._activeEffects.length;)this.popEffect();t.forEach(function(B){return r.applyEffect(B)})},A.prototype.applyEffect=function(t){this.ctx.save(),iQ(t)&&(this.ctx.globalAlpha=t.opacity),aQ(t)&&(this.ctx.translate(t.offsetX,t.offsetY),this.ctx.transform(t.matrix[0],t.matrix[1],t.matrix[2],t.matrix[3],t.matrix[4],t.matrix[5]),this.ctx.translate(-t.offsetX,-t.offsetY)),en(t)&&(this.path(t.path),this.ctx.clip()),this._activeEffects.push(t)},A.prototype.popEffect=function(){this._activeEffects.pop(),this.ctx.restore()},A.prototype.renderStack=function(t){return P(this,void 0,void 0,function(){var r;return _(this,function(B){switch(B.label){case 0:return r=t.element.container.styles,r.isVisible()?[4,this.renderStackContent(t)]:[3,2];case 1:B.sent(),B.label=2;case 2:return[2]}})})},A.prototype.renderNode=function(t){return P(this,void 0,void 0,function(){return _(this,function(r){switch(r.label){case 0:if(G(t.container.flags,16))debugger;return t.container.styles.isVisible()?[4,this.renderNodeBackgroundAndBorders(t)]:[3,3];case 1:return r.sent(),[4,this.renderNodeContent(t)];case 2:r.sent(),r.label=3;case 3:return[2]}})})},A.prototype.renderTextWithLetterSpacing=function(t,r,B){var n=this;if(r===0)this.ctx.fillText(t.text,t.bounds.left,t.bounds.top+B);else{var s=rt(t.text);s.reduce(function(i,a){return n.ctx.fillText(a,i,t.bounds.top+B),i+n.ctx.measureText(a).width},t.bounds.left)}},A.prototype.createFontStyle=function(t){var r=t.fontVariant.filter(function(s){return s==="normal"||s==="small-caps"}).join(""),B=vQ(t.fontFamily).join(", "),n=Ce(t.fontSize)?""+t.fontSize.number+t.fontSize.unit:t.fontSize.number+"px";return[[t.fontStyle,r,t.fontWeight,n,B].join(" "),B,n]},A.prototype.renderTextNode=function(t,r){return P(this,void 0,void 0,function(){var B,n,s,i,a,o,Q,g,w=this;return _(this,function(f){return B=this.createFontStyle(r),n=B[0],s=B[1],i=B[2],this.ctx.font=n,this.ctx.direction=r.direction===1?"rtl":"ltr",this.ctx.textAlign="left",this.ctx.textBaseline="alphabetic",a=this.fontMetrics.getMetrics(s,i),o=a.baseline,Q=a.middle,g=r.paintOrder,t.textBounds.forEach(function(c){g.forEach(function(C){switch(C){case 0:w.ctx.fillStyle=R(r.color),w.renderTextWithLetterSpacing(c,r.letterSpacing,o);var H=r.textShadow;H.length&&c.text.trim().length&&(H.slice(0).reverse().forEach(function(h){w.ctx.shadowColor=R(h.color),w.ctx.shadowOffsetX=h.offsetX.number*w.options.scale,w.ctx.shadowOffsetY=h.offsetY.number*w.options.scale,w.ctx.shadowBlur=h.blur.number,w.renderTextWithLetterSpacing(c,r.letterSpacing,o)}),w.ctx.shadowColor="",w.ctx.shadowOffsetX=0,w.ctx.shadowOffsetY=0,w.ctx.shadowBlur=0),r.textDecorationLine.length&&(w.ctx.fillStyle=R(r.textDecorationColor||r.color),r.textDecorationLine.forEach(function(h){switch(h){case 1:w.ctx.fillRect(c.bounds.left,Math.round(c.bounds.top+o),c.bounds.width,1);break;case 2:w.ctx.fillRect(c.bounds.left,Math.round(c.bounds.top),c.bounds.width,1);break;case 3:w.ctx.fillRect(c.bounds.left,Math.ceil(c.bounds.top+Q),c.bounds.width,1);break}}));break;case 1:r.webkitTextStrokeWidth&&c.text.trim().length&&(w.ctx.strokeStyle=R(r.webkitTextStrokeColor),w.ctx.lineWidth=r.webkitTextStrokeWidth,w.ctx.lineJoin=window.chrome?"miter":"round",w.ctx.strokeText(c.text,c.bounds.left,c.bounds.top+o)),w.ctx.strokeStyle="",w.ctx.lineWidth=0,w.ctx.lineJoin="miter";break}})}),[2]})})},A.prototype.renderReplacedElement=function(t,r,B){if(B&&t.intrinsicWidth>0&&t.intrinsicHeight>0){var n=je(t),s=qe(r);this.path(s),this.ctx.save(),this.ctx.clip(),this.ctx.drawImage(B,0,0,t.intrinsicWidth,t.intrinsicHeight,n.left,n.top,n.width,n.height),this.ctx.restore()}},A.prototype.renderNodeContent=function(t){return P(this,void 0,void 0,function(){var r,B,n,s,i,a,F,F,o,Q,g,w,p,f,c,d,C,H,h,F,K,p,d;return _(this,function(l){switch(l.label){case 0:this.applyEffects(t.getEffects(4)),r=t.container,B=t.curves,n=r.styles,s=0,i=r.textNodes,l.label=1;case 1:return s0&&V>0&&(y=n.ctx.createPattern(d,"repeat"),n.renderRepeat(O,y,Y,W))):aa(Q)&&(E=Kr(t,r,[null,null,null]),O=E[0],k=E[1],q=E[2],L=E[3],V=E[4],CA=Q.position.length===0?[$r]:Q.position,Y=b(CA[0],L),W=b(CA[CA.length-1],V),IA=ra(Q,Y,W,L,V),BA=IA[0],uA=IA[1],BA>0&&uA>0&&(vA=n.ctx.createRadialGradient(k+Y,q+W,0,k+Y,q+W,BA),It(Q.stops,BA*2).forEach(function(YA){return vA.addColorStop(YA.stop,R(YA.color))}),n.path(O),n.ctx.fillStyle=vA,BA!==uA?(yA=t.bounds.left+.5*t.bounds.width,iA=t.bounds.top+.5*t.bounds.height,KA=uA/BA,oA=1/KA,n.ctx.save(),n.ctx.translate(yA,iA),n.ctx.transform(1,0,0,KA,0,0),n.ctx.translate(-yA,-iA),n.ctx.fillRect(k,oA*(q-iA)+iA,L,V*oA),n.ctx.restore()):n.ctx.fill())),TA.label=6;case 6:return r--,[2]}})},n=this,s=0,i=t.styles.backgroundImage.slice(0).reverse(),o.label=1;case 1:return s0?Q.style!==2?[3,5]:[4,this.renderDashedDottedBorder(Q.color,Q.width,i,t.curves,2)]:[3,11]):[3,13];case 4:return w.sent(),[3,11];case 5:return Q.style!==3?[3,7]:[4,this.renderDashedDottedBorder(Q.color,Q.width,i,t.curves,3)];case 6:return w.sent(),[3,11];case 7:return Q.style!==4?[3,9]:[4,this.renderDoubleBorder(Q.color,Q.width,i,t.curves)];case 8:return w.sent(),[3,11];case 9:return[4,this.renderSolidBorder(Q.color,i,t.curves)];case 10:w.sent(),w.label=11;case 11:i++,w.label=12;case 12:return a++,[3,3];case 13:return[2]}})})},A.prototype.renderDashedDottedBorder=function(t,r,B,n,s){return P(this,void 0,void 0,function(){var i,a,o,Q,g,w,f,c,C,H,h,F,K,p,d,l,d,l;return _(this,function(v){return this.ctx.save(),i=cQ(n,B),a=rB(n,B),s===2&&(this.path(a),this.ctx.clip()),$(a[0])?(o=a[0].start.x,Q=a[0].start.y):(o=a[0].x,Q=a[0].y),$(a[1])?(g=a[1].end.x,w=a[1].end.y):(g=a[1].x,w=a[1].y),B===0||B===2?f=Math.abs(o-g):f=Math.abs(Q-w),this.ctx.beginPath(),s===3?this.formatPath(i):this.formatPath(a.slice(0,2)),c=r<3?r*3:r*2,C=r<3?r*2:r,s===3&&(c=r,C=r),H=!0,f<=c*2?H=!1:f<=c*2+C?(h=f/(2*c+C),c*=h,C*=h):(F=Math.floor((f+C)/(c+C)),K=(f-F*c)/(F-1),p=(f-(F+1)*c)/F,C=p<=0||Math.abs(C-K)\n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\n/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nfunction __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nvar __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n };\r\n return __assign.apply(this, arguments);\r\n};\r\n\r\nfunction __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nfunction __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nfunction __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || from);\r\n}\n\nvar Bounds = /** @class */ (function () {\n function Bounds(left, top, width, height) {\n this.left = left;\n this.top = top;\n this.width = width;\n this.height = height;\n }\n Bounds.prototype.add = function (x, y, w, h) {\n return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h);\n };\n Bounds.fromClientRect = function (context, clientRect) {\n return new Bounds(clientRect.left + context.windowBounds.left, clientRect.top + context.windowBounds.top, clientRect.width, clientRect.height);\n };\n Bounds.fromDOMRectList = function (context, domRectList) {\n var domRect = Array.from(domRectList).find(function (rect) { return rect.width !== 0; });\n return domRect\n ? new Bounds(domRect.left + context.windowBounds.left, domRect.top + context.windowBounds.top, domRect.width, domRect.height)\n : Bounds.EMPTY;\n };\n Bounds.EMPTY = new Bounds(0, 0, 0, 0);\n return Bounds;\n}());\nvar parseBounds = function (context, node) {\n return Bounds.fromClientRect(context, node.getBoundingClientRect());\n};\nvar parseDocumentSize = function (document) {\n var body = document.body;\n var documentElement = document.documentElement;\n if (!body || !documentElement) {\n throw new Error(\"Unable to get document size\");\n }\n var width = Math.max(Math.max(body.scrollWidth, documentElement.scrollWidth), Math.max(body.offsetWidth, documentElement.offsetWidth), Math.max(body.clientWidth, documentElement.clientWidth));\n var height = Math.max(Math.max(body.scrollHeight, documentElement.scrollHeight), Math.max(body.offsetHeight, documentElement.offsetHeight), Math.max(body.clientHeight, documentElement.clientHeight));\n return new Bounds(0, 0, width, height);\n};\n\n/*\n * css-line-break 2.1.0 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar toCodePoints$1 = function (str) {\n var codePoints = [];\n var i = 0;\n var length = str.length;\n while (i < length) {\n var value = str.charCodeAt(i++);\n if (value >= 0xd800 && value <= 0xdbff && i < length) {\n var extra = str.charCodeAt(i++);\n if ((extra & 0xfc00) === 0xdc00) {\n codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);\n }\n else {\n codePoints.push(value);\n i--;\n }\n }\n else {\n codePoints.push(value);\n }\n }\n return codePoints;\n};\nvar fromCodePoint$1 = function () {\n var codePoints = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n codePoints[_i] = arguments[_i];\n }\n if (String.fromCodePoint) {\n return String.fromCodePoint.apply(String, codePoints);\n }\n var length = codePoints.length;\n if (!length) {\n return '';\n }\n var codeUnits = [];\n var index = -1;\n var result = '';\n while (++index < length) {\n var codePoint = codePoints[index];\n if (codePoint <= 0xffff) {\n codeUnits.push(codePoint);\n }\n else {\n codePoint -= 0x10000;\n codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00);\n }\n if (index + 1 === length || codeUnits.length > 0x4000) {\n result += String.fromCharCode.apply(String, codeUnits);\n codeUnits.length = 0;\n }\n }\n return result;\n};\nvar chars$2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nvar lookup$2 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (var i$2 = 0; i$2 < chars$2.length; i$2++) {\n lookup$2[chars$2.charCodeAt(i$2)] = i$2;\n}\n\n/*\n * utrie 1.0.2 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar chars$1$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nvar lookup$1$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (var i$1$1 = 0; i$1$1 < chars$1$1.length; i$1$1++) {\n lookup$1$1[chars$1$1.charCodeAt(i$1$1)] = i$1$1;\n}\nvar decode$1 = function (base64) {\n var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;\n if (base64[base64.length - 1] === '=') {\n bufferLength--;\n if (base64[base64.length - 2] === '=') {\n bufferLength--;\n }\n }\n var buffer = typeof ArrayBuffer !== 'undefined' &&\n typeof Uint8Array !== 'undefined' &&\n typeof Uint8Array.prototype.slice !== 'undefined'\n ? new ArrayBuffer(bufferLength)\n : new Array(bufferLength);\n var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer);\n for (i = 0; i < len; i += 4) {\n encoded1 = lookup$1$1[base64.charCodeAt(i)];\n encoded2 = lookup$1$1[base64.charCodeAt(i + 1)];\n encoded3 = lookup$1$1[base64.charCodeAt(i + 2)];\n encoded4 = lookup$1$1[base64.charCodeAt(i + 3)];\n bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);\n bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);\n bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);\n }\n return buffer;\n};\nvar polyUint16Array$1 = function (buffer) {\n var length = buffer.length;\n var bytes = [];\n for (var i = 0; i < length; i += 2) {\n bytes.push((buffer[i + 1] << 8) | buffer[i]);\n }\n return bytes;\n};\nvar polyUint32Array$1 = function (buffer) {\n var length = buffer.length;\n var bytes = [];\n for (var i = 0; i < length; i += 4) {\n bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]);\n }\n return bytes;\n};\n\n/** Shift size for getting the index-2 table offset. */\nvar UTRIE2_SHIFT_2$1 = 5;\n/** Shift size for getting the index-1 table offset. */\nvar UTRIE2_SHIFT_1$1 = 6 + 5;\n/**\n * Shift size for shifting left the index array values.\n * Increases possible data size with 16-bit index values at the cost\n * of compactability.\n * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY.\n */\nvar UTRIE2_INDEX_SHIFT$1 = 2;\n/**\n * Difference between the two shift sizes,\n * for getting an index-1 offset from an index-2 offset. 6=11-5\n */\nvar UTRIE2_SHIFT_1_2$1 = UTRIE2_SHIFT_1$1 - UTRIE2_SHIFT_2$1;\n/**\n * The part of the index-2 table for U+D800..U+DBFF stores values for\n * lead surrogate code _units_ not code _points_.\n * Values for lead surrogate code _points_ are indexed with this portion of the table.\n * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.)\n */\nvar UTRIE2_LSCP_INDEX_2_OFFSET$1 = 0x10000 >> UTRIE2_SHIFT_2$1;\n/** Number of entries in a data block. 32=0x20 */\nvar UTRIE2_DATA_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_2$1;\n/** Mask for getting the lower bits for the in-data-block offset. */\nvar UTRIE2_DATA_MASK$1 = UTRIE2_DATA_BLOCK_LENGTH$1 - 1;\nvar UTRIE2_LSCP_INDEX_2_LENGTH$1 = 0x400 >> UTRIE2_SHIFT_2$1;\n/** Count the lengths of both BMP pieces. 2080=0x820 */\nvar UTRIE2_INDEX_2_BMP_LENGTH$1 = UTRIE2_LSCP_INDEX_2_OFFSET$1 + UTRIE2_LSCP_INDEX_2_LENGTH$1;\n/**\n * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.\n * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2.\n */\nvar UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 = UTRIE2_INDEX_2_BMP_LENGTH$1;\nvar UTRIE2_UTF8_2B_INDEX_2_LENGTH$1 = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */\n/**\n * The index-1 table, only used for supplementary code points, at offset 2112=0x840.\n * Variable length, for code points up to highStart, where the last single-value range starts.\n * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1.\n * (For 0x100000 supplementary code points U+10000..U+10ffff.)\n *\n * The part of the index-2 table for supplementary code points starts\n * after this index-1 table.\n *\n * Both the index-1 table and the following part of the index-2 table\n * are omitted completely if there is only BMP data.\n */\nvar UTRIE2_INDEX_1_OFFSET$1 = UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 + UTRIE2_UTF8_2B_INDEX_2_LENGTH$1;\n/**\n * Number of index-1 entries for the BMP. 32=0x20\n * This part of the index-1 table is omitted from the serialized form.\n */\nvar UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 = 0x10000 >> UTRIE2_SHIFT_1$1;\n/** Number of entries in an index-2 block. 64=0x40 */\nvar UTRIE2_INDEX_2_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_1_2$1;\n/** Mask for getting the lower bits for the in-index-2-block offset. */\nvar UTRIE2_INDEX_2_MASK$1 = UTRIE2_INDEX_2_BLOCK_LENGTH$1 - 1;\nvar slice16$1 = function (view, start, end) {\n if (view.slice) {\n return view.slice(start, end);\n }\n return new Uint16Array(Array.prototype.slice.call(view, start, end));\n};\nvar slice32$1 = function (view, start, end) {\n if (view.slice) {\n return view.slice(start, end);\n }\n return new Uint32Array(Array.prototype.slice.call(view, start, end));\n};\nvar createTrieFromBase64$1 = function (base64, _byteLength) {\n var buffer = decode$1(base64);\n var view32 = Array.isArray(buffer) ? polyUint32Array$1(buffer) : new Uint32Array(buffer);\n var view16 = Array.isArray(buffer) ? polyUint16Array$1(buffer) : new Uint16Array(buffer);\n var headerLength = 24;\n var index = slice16$1(view16, headerLength / 2, view32[4] / 2);\n var data = view32[5] === 2\n ? slice16$1(view16, (headerLength + view32[4]) / 2)\n : slice32$1(view32, Math.ceil((headerLength + view32[4]) / 4));\n return new Trie$1(view32[0], view32[1], view32[2], view32[3], index, data);\n};\nvar Trie$1 = /** @class */ (function () {\n function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) {\n this.initialValue = initialValue;\n this.errorValue = errorValue;\n this.highStart = highStart;\n this.highValueIndex = highValueIndex;\n this.index = index;\n this.data = data;\n }\n /**\n * Get the value for a code point as stored in the Trie.\n *\n * @param codePoint the code point\n * @return the value\n */\n Trie.prototype.get = function (codePoint) {\n var ix;\n if (codePoint >= 0) {\n if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) {\n // Ordinary BMP code point, excluding leading surrogates.\n // BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index.\n // 16 bit data is stored in the index array itself.\n ix = this.index[codePoint >> UTRIE2_SHIFT_2$1];\n ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);\n return this.data[ix];\n }\n if (codePoint <= 0xffff) {\n // Lead Surrogate Code Point. A Separate index section is stored for\n // lead surrogate code units and code points.\n // The main index has the code unit data.\n // For this function, we need the code point data.\n // Note: this expression could be refactored for slightly improved efficiency, but\n // surrogate code points will be so rare in practice that it's not worth it.\n ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET$1 + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2$1)];\n ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);\n return this.data[ix];\n }\n if (codePoint < this.highStart) {\n // Supplemental code point, use two-level lookup.\n ix = UTRIE2_INDEX_1_OFFSET$1 - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 + (codePoint >> UTRIE2_SHIFT_1$1);\n ix = this.index[ix];\n ix += (codePoint >> UTRIE2_SHIFT_2$1) & UTRIE2_INDEX_2_MASK$1;\n ix = this.index[ix];\n ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);\n return this.data[ix];\n }\n if (codePoint <= 0x10ffff) {\n return this.data[this.highValueIndex];\n }\n }\n // Fall through. The code point is outside of the legal range of 0..0x10ffff.\n return this.errorValue;\n };\n return Trie;\n}());\n\n/*\n * base64-arraybuffer 1.0.2 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar chars$3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nvar lookup$3 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (var i$3 = 0; i$3 < chars$3.length; i$3++) {\n lookup$3[chars$3.charCodeAt(i$3)] = i$3;\n}\n\nvar base64$1 = 'KwAAAAAAAAAACA4AUD0AADAgAAACAAAAAAAIABAAGABAAEgAUABYAGAAaABgAGgAYgBqAF8AZwBgAGgAcQB5AHUAfQCFAI0AlQCdAKIAqgCyALoAYABoAGAAaABgAGgAwgDKAGAAaADGAM4A0wDbAOEA6QDxAPkAAQEJAQ8BFwF1AH0AHAEkASwBNAE6AUIBQQFJAVEBWQFhAWgBcAF4ATAAgAGGAY4BlQGXAZ8BpwGvAbUBvQHFAc0B0wHbAeMB6wHxAfkBAQIJAvEBEQIZAiECKQIxAjgCQAJGAk4CVgJeAmQCbAJ0AnwCgQKJApECmQKgAqgCsAK4ArwCxAIwAMwC0wLbAjAA4wLrAvMC+AIAAwcDDwMwABcDHQMlAy0DNQN1AD0DQQNJA0kDSQNRA1EDVwNZA1kDdQB1AGEDdQBpA20DdQN1AHsDdQCBA4kDkQN1AHUAmQOhA3UAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AKYDrgN1AHUAtgO+A8YDzgPWAxcD3gPjA+sD8wN1AHUA+wMDBAkEdQANBBUEHQQlBCoEFwMyBDgEYABABBcDSARQBFgEYARoBDAAcAQzAXgEgASIBJAEdQCXBHUAnwSnBK4EtgS6BMIEyAR1AHUAdQB1AHUAdQCVANAEYABgAGAAYABgAGAAYABgANgEYADcBOQEYADsBPQE/AQEBQwFFAUcBSQFLAU0BWQEPAVEBUsFUwVbBWAAYgVgAGoFcgV6BYIFigWRBWAAmQWfBaYFYABgAGAAYABgAKoFYACxBbAFuQW6BcEFwQXHBcEFwQXPBdMF2wXjBeoF8gX6BQIGCgYSBhoGIgYqBjIGOgZgAD4GRgZMBmAAUwZaBmAAYABgAGAAYABgAGAAYABgAGAAYABgAGIGYABpBnAGYABgAGAAYABgAGAAYABgAGAAYAB4Bn8GhQZgAGAAYAB1AHcDFQSLBmAAYABgAJMGdQA9A3UAmwajBqsGqwaVALMGuwbDBjAAywbSBtIG1QbSBtIG0gbSBtIG0gbdBuMG6wbzBvsGAwcLBxMHAwcbByMHJwcsBywHMQcsB9IGOAdAB0gHTgfSBkgHVgfSBtIG0gbSBtIG0gbSBtIG0gbSBiwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdgAGAALAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdbB2MHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB2kH0gZwB64EdQB1AHUAdQB1AHUAdQB1AHUHfQdgAIUHjQd1AHUAlQedB2AAYAClB6sHYACzB7YHvgfGB3UAzgfWBzMB3gfmB1EB7gf1B/0HlQENAQUIDQh1ABUIHQglCBcDLQg1CD0IRQhNCEEDUwh1AHUAdQBbCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIcAh3CHoIMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIgggwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAALAcsBywHLAcsBywHLAcsBywHLAcsB4oILAcsB44I0gaWCJ4Ipgh1AHUAqgiyCHUAdQB1AHUAdQB1AHUAdQB1AHUAtwh8AXUAvwh1AMUIyQjRCNkI4AjoCHUAdQB1AO4I9gj+CAYJDgkTCS0HGwkjCYIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiAAIAAAAFAAYABgAGIAXwBgAHEAdQBFAJUAogCyAKAAYABgAEIA4ABGANMA4QDxAMEBDwE1AFwBLAE6AQEBUQF4QkhCmEKoQrhCgAHIQsAB0MLAAcABwAHAAeDC6ABoAHDCwMMAAcABwAHAAdDDGMMAAcAB6MM4wwjDWMNow3jDaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEjDqABWw6bDqABpg6gAaABoAHcDvwOPA+gAaABfA/8DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DpcPAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcAB9cPKwkyCToJMAB1AHUAdQBCCUoJTQl1AFUJXAljCWcJawkwADAAMAAwAHMJdQB2CX4JdQCECYoJjgmWCXUAngkwAGAAYABxAHUApgn3A64JtAl1ALkJdQDACTAAMAAwADAAdQB1AHUAdQB1AHUAdQB1AHUAowYNBMUIMAAwADAAMADICcsJ0wnZCRUE4QkwAOkJ8An4CTAAMAB1AAAKvwh1AAgKDwoXCh8KdQAwACcKLgp1ADYKqAmICT4KRgowADAAdQB1AE4KMAB1AFYKdQBeCnUAZQowADAAMAAwADAAMAAwADAAMAAVBHUAbQowADAAdQC5CXUKMAAwAHwBxAijBogEMgF9CoQKiASMCpQKmgqIBKIKqgquCogEDQG2Cr4KxgrLCjAAMADTCtsKCgHjCusK8Qr5CgELMAAwADAAMAB1AIsECQsRC3UANAEZCzAAMAAwADAAMAB1ACELKQswAHUANAExCzkLdQBBC0kLMABRC1kLMAAwADAAMAAwADAAdQBhCzAAMAAwAGAAYABpC3ELdwt/CzAAMACHC4sLkwubC58Lpwt1AK4Ltgt1APsDMAAwADAAMAAwADAAMAAwAL4LwwvLC9IL1wvdCzAAMADlC+kL8Qv5C/8LSQswADAAMAAwADAAMAAwADAAMAAHDDAAMAAwADAAMAAODBYMHgx1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1ACYMMAAwADAAdQB1AHUALgx1AHUAdQB1AHUAdQA2DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AD4MdQBGDHUAdQB1AHUAdQB1AEkMdQB1AHUAdQB1AFAMMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQBYDHUAdQB1AF8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUA+wMVBGcMMAAwAHwBbwx1AHcMfwyHDI8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAYABgAJcMMAAwADAAdQB1AJ8MlQClDDAAMACtDCwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB7UMLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AA0EMAC9DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAsBywHLAcsBywHLAcsBywHLQcwAMEMyAwsBywHLAcsBywHLAcsBywHLAcsBywHzAwwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1ANQM2QzhDDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMABgAGAAYABgAGAAYABgAOkMYADxDGAA+AwADQYNYABhCWAAYAAODTAAMAAwADAAFg1gAGAAHg37AzAAMAAwADAAYABgACYNYAAsDTQNPA1gAEMNPg1LDWAAYABgAGAAYABgAGAAYABgAGAAUg1aDYsGVglhDV0NcQBnDW0NdQ15DWAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAlQCBDZUAiA2PDZcNMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAnw2nDTAAMAAwADAAMAAwAHUArw23DTAAMAAwADAAMAAwADAAMAAwADAAMAB1AL8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQDHDTAAYABgAM8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA1w11ANwNMAAwAD0B5A0wADAAMAAwADAAMADsDfQN/A0EDgwOFA4wABsOMAAwADAAMAAwADAAMAAwANIG0gbSBtIG0gbSBtIG0gYjDigOwQUuDsEFMw7SBjoO0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGQg5KDlIOVg7SBtIGXg5lDm0OdQ7SBtIGfQ6EDooOjQ6UDtIGmg6hDtIG0gaoDqwO0ga0DrwO0gZgAGAAYADEDmAAYAAkBtIGzA5gANIOYADaDokO0gbSBt8O5w7SBu8O0gb1DvwO0gZgAGAAxA7SBtIG0gbSBtIGYABgAGAAYAAED2AAsAUMD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHJA8sBywHLAcsBywHLAccDywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywPLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAc0D9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHPA/SBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gYUD0QPlQCVAJUAMAAwADAAMACVAJUAlQCVAJUAlQCVAEwPMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA//8EAAQABAAEAAQABAAEAAQABAANAAMAAQABAAIABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQACgATABcAHgAbABoAHgAXABYAEgAeABsAGAAPABgAHABLAEsASwBLAEsASwBLAEsASwBLABgAGAAeAB4AHgATAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABYAGwASAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWAA0AEQAeAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAFAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJABYAGgAbABsAGwAeAB0AHQAeAE8AFwAeAA0AHgAeABoAGwBPAE8ADgBQAB0AHQAdAE8ATwAXAE8ATwBPABYAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAFAATwBAAE8ATwBPAEAATwBQAFAATwBQAB4AHgAeAB4AHgAeAB0AHQAdAB0AHgAdAB4ADgBQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgBQAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAkACQAJAAkACQAJAAkABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAFAAHgAeAB4AKwArAFAAUABQAFAAGABQACsAKwArACsAHgAeAFAAHgBQAFAAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUAAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAYAA0AKwArAB4AHgAbACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAB4ABAAEAB4ABAAEABMABAArACsAKwArACsAKwArACsAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAKwArACsAKwBWAFYAVgBWAB4AHgArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AGgAaABoAGAAYAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQAEwAEACsAEwATAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABLAEsASwBLAEsASwBLAEsASwBLABoAGQAZAB4AUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABMAUAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABABQAFAABAAEAB4ABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUAAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAFAABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQAUABQAB4AHgAYABMAUAArACsABAAbABsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAFAABAAEAAQABAAEAFAABAAEAAQAUAAEAAQABAAEAAQAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArACsAHgArAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAUAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEAA0ADQBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUAArACsAKwBQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABABQACsAKwArACsAKwArACsAKwAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUAAaABoAUABQAFAAUABQAEwAHgAbAFAAHgAEACsAKwAEAAQABAArAFAAUABQAFAAUABQACsAKwArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQACsAUABQACsAKwAEACsABAAEAAQABAAEACsAKwArACsABAAEACsAKwAEAAQABAArACsAKwAEACsAKwArACsAKwArACsAUABQAFAAUAArAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLAAQABABQAFAAUAAEAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAArACsAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AGwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAKwArACsAKwArAAQABAAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAAQAUAArAFAAUABQAFAAUABQACsAKwArAFAAUABQACsAUABQAFAAUAArACsAKwBQAFAAKwBQACsAUABQACsAKwArAFAAUAArACsAKwBQAFAAUAArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArAAQABAAEAAQABAArACsAKwAEAAQABAArAAQABAAEAAQAKwArAFAAKwArACsAKwArACsABAArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAHgAeAB4AHgAeAB4AGwAeACsAKwArACsAKwAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAUABQAFAAKwArACsAKwArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwAOAFAAUABQAFAAUABQAFAAHgBQAAQABAAEAA4AUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAKwArAAQAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAKwArACsAKwArACsAUAArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAFAABAAEAAQABAAEAAQABAArAAQABAAEACsABAAEAAQABABQAB4AKwArACsAKwBQAFAAUAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQABoAUABQAFAAUABQAFAAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQACsAUAArACsAUABQAFAAUABQAFAAUAArACsAKwAEACsAKwArACsABAAEAAQABAAEAAQAKwAEACsABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArAAQABAAeACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAXAAqACoAKgAqACoAKgAqACsAKwArACsAGwBcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAeAEsASwBLAEsASwBLAEsASwBLAEsADQANACsAKwArACsAKwBcAFwAKwBcACsAXABcAFwAXABcACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAXAArAFwAXABcAFwAXABcAFwAXABcAFwAKgBcAFwAKgAqACoAKgAqACoAKgAqACoAXAArACsAXABcAFwAXABcACsAXAArACoAKgAqACoAKgAqACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwBcAFwAXABcAFAADgAOAA4ADgAeAA4ADgAJAA4ADgANAAkAEwATABMAEwATAAkAHgATAB4AHgAeAAQABAAeAB4AHgAeAB4AHgBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQAFAADQAEAB4ABAAeAAQAFgARABYAEQAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAAQABAAEAAQADQAEAAQAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAA0ADQAeAB4AHgAeAB4AHgAEAB4AHgAeAB4AHgAeACsAHgAeAA4ADgANAA4AHgAeAB4AHgAeAAkACQArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgBcAEsASwBLAEsASwBLAEsASwBLAEsADQANAB4AHgAeAB4AXABcAFwAXABcAFwAKgAqACoAKgBcAFwAXABcACoAKgAqAFwAKgAqACoAXABcACoAKgAqACoAKgAqACoAXABcAFwAKgAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqAFwAKgBLAEsASwBLAEsASwBLAEsASwBLACoAKgAqACoAKgAqAFAAUABQAFAAUABQACsAUAArACsAKwArACsAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAKwBQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsABAAEAAQAHgANAB4AHgAeAB4AHgAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUAArACsADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWABEAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQANAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAANAA0AKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUAArAAQABAArACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqAA0ADQAVAFwADQAeAA0AGwBcACoAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwAeAB4AEwATAA0ADQAOAB4AEwATAB4ABAAEAAQACQArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAHgArACsAKwATABMASwBLAEsASwBLAEsASwBLAEsASwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAXABcAFwAXABcACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAXAArACsAKwAqACoAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsAHgAeAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKwAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKwArAAQASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACoAKgAqACoAKgAqACoAXAAqACoAKgAqACoAKgArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABABQAFAAUABQAFAAUABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwANAA0AHgANAA0ADQANAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwAeAB4AHgAeAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArAA0ADQANAA0ADQBLAEsASwBLAEsASwBLAEsASwBLACsAKwArAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUAAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAAQAUABQAFAAUABQAFAABABQAFAABAAEAAQAUAArACsAKwArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQACsAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAFAAUABQACsAHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQACsAKwAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQACsAHgAeAB4AHgAeAB4AHgAOAB4AKwANAA0ADQANAA0ADQANAAkADQANAA0ACAAEAAsABAAEAA0ACQANAA0ADAAdAB0AHgAXABcAFgAXABcAFwAWABcAHQAdAB4AHgAUABQAFAANAAEAAQAEAAQABAAEAAQACQAaABoAGgAaABoAGgAaABoAHgAXABcAHQAVABUAHgAeAB4AHgAeAB4AGAAWABEAFQAVABUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ADQAeAA0ADQANAA0AHgANAA0ADQAHAB4AHgAeAB4AKwAEAAQABAAEAAQABAAEAAQABAAEAFAAUAArACsATwBQAFAAUABQAFAAHgAeAB4AFgARAE8AUABPAE8ATwBPAFAAUABQAFAAUAAeAB4AHgAWABEAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArABsAGwAbABsAGwAbABsAGgAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGgAbABsAGwAbABoAGwAbABoAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAHgAeAFAAGgAeAB0AHgBQAB4AGgAeAB4AHgAeAB4AHgAeAB4AHgBPAB4AUAAbAB4AHgBQAFAAUABQAFAAHgAeAB4AHQAdAB4AUAAeAFAAHgBQAB4AUABPAFAAUAAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgBQAFAAUABQAE8ATwBQAFAAUABQAFAATwBQAFAATwBQAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAUABQAFAATwBPAE8ATwBPAE8ATwBPAE8ATwBQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABPAB4AHgArACsAKwArAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHQAdAB4AHgAeAB0AHQAeAB4AHQAeAB4AHgAdAB4AHQAbABsAHgAdAB4AHgAeAB4AHQAeAB4AHQAdAB0AHQAeAB4AHQAeAB0AHgAdAB0AHQAdAB0AHQAeAB0AHgAeAB4AHgAeAB0AHQAdAB0AHgAeAB4AHgAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHgAeAB0AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAeAB0AHQAdAB0AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAdAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAWABEAHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAWABEAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AHQAdAB0AHgAeAB0AHgAeAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlAB4AHQAdAB4AHgAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AJQAlAB0AHQAlAB4AJQAlACUAIAAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAdAB0AHQAeAB0AJQAdAB0AHgAdAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAdAB0AHQAdACUAHgAlACUAJQAdACUAJQAdAB0AHQAlACUAHQAdACUAHQAdACUAJQAlAB4AHQAeAB4AHgAeAB0AHQAlAB0AHQAdAB0AHQAdACUAJQAlACUAJQAdACUAJQAgACUAHQAdACUAJQAlACUAJQAlACUAJQAeAB4AHgAlACUAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AFwAXABcAFwAXABcAHgATABMAJQAeAB4AHgAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARABYAEQAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAEAAQABAAeAB4AKwArACsAKwArABMADQANAA0AUAATAA0AUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUAANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAA0ADQANAA0ADQANAA0ADQAeAA0AFgANAB4AHgAXABcAHgAeABcAFwAWABEAFgARABYAEQAWABEADQANAA0ADQATAFAADQANAB4ADQANAB4AHgAeAB4AHgAMAAwADQANAA0AHgANAA0AFgANAA0ADQANAA0ADQANAA0AHgANAB4ADQANAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArAA0AEQARACUAJQBHAFcAVwAWABEAFgARABYAEQAWABEAFgARACUAJQAWABEAFgARABYAEQAWABEAFQAWABEAEQAlAFcAVwBXAFcAVwBXAFcAVwBXAAQABAAEAAQABAAEACUAVwBXAFcAVwA2ACUAJQBXAFcAVwBHAEcAJQAlACUAKwBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBRAFcAUQBXAFEAVwBXAFcAVwBXAFcAUQBXAFcAVwBXAFcAVwBRAFEAKwArAAQABAAVABUARwBHAFcAFQBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBRAFcAVwBXAFcAVwBXAFEAUQBXAFcAVwBXABUAUQBHAEcAVwArACsAKwArACsAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwAlACUAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACsAKwArACsAKwArACsAKwArACsAKwArAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBPAE8ATwBPAE8ATwBPAE8AJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADQATAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABLAEsASwBLAEsASwBLAEsASwBLAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAABAAEAAQABAAeAAQABAAEAAQABAAEAAQABAAEAAQAHgBQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAeAA0ADQANAA0ADQArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAAQAUABQAFAABABQAFAAUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAeAB4AHgAeAAQAKwArACsAUABQAFAAUABQAFAAHgAeABoAHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADgAOABMAEwArACsAKwArACsAKwArACsABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwANAA0ASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUAAeAB4AHgBQAA4AUABQAAQAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArAB4AWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYACsAKwArAAQAHgAeAB4AHgAeAB4ADQANAA0AHgAeAB4AHgArAFAASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArAB4AHgBcAFwAXABcAFwAKgBcAFwAXABcAFwAXABcAFwAXABcAEsASwBLAEsASwBLAEsASwBLAEsAXABcAFwAXABcACsAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAFAAUABQAAQAUABQAFAAUABQAFAAUABQAAQABAArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAHgANAA0ADQBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAXAAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAKgAqACoAXABcACoAKgBcAFwAXABcAFwAKgAqAFwAKgBcACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcACoAKgBQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAA0ADQBQAFAAUAAEAAQAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQADQAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAVABVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBUAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVACsAKwArACsAKwArACsAKwArACsAKwArAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAKwArACsAKwBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAKwArACsAKwAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAKwArACsAKwArAFYABABWAFYAVgBWAFYAVgBWAFYAVgBWAB4AVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgArAFYAVgBWAFYAVgArAFYAKwBWAFYAKwBWAFYAKwBWAFYAVgBWAFYAVgBWAFYAVgBWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAEQAWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAaAB4AKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAGAARABEAGAAYABMAEwAWABEAFAArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACUAJQAlACUAJQAWABEAFgARABYAEQAWABEAFgARABYAEQAlACUAFgARACUAJQAlACUAJQAlACUAEQAlABEAKwAVABUAEwATACUAFgARABYAEQAWABEAJQAlACUAJQAlACUAJQAlACsAJQAbABoAJQArACsAKwArAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAcAKwATACUAJQAbABoAJQAlABYAEQAlACUAEQAlABEAJQBXAFcAVwBXAFcAVwBXAFcAVwBXABUAFQAlACUAJQATACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXABYAJQARACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAWACUAEQAlABYAEQARABYAEQARABUAVwBRAFEAUQBRAFEAUQBRAFEAUQBRAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcARwArACsAVwBXAFcAVwBXAFcAKwArAFcAVwBXAFcAVwBXACsAKwBXAFcAVwBXAFcAVwArACsAVwBXAFcAKwArACsAGgAbACUAJQAlABsAGwArAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAAQAB0AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsADQANAA0AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAA0AUABQAFAAUAArACsAKwArAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwArAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwBQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAUABQAFAAUABQAAQABAAEACsABAAEACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAKwBQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAA0ADQANAA0ADQANAA0ADQAeACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAArACsAKwArAFAAUABQAFAAUAANAA0ADQANAA0ADQAUACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsADQANAA0ADQANAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArAAQABAANACsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAB4AHgAeAB4AHgArACsAKwArACsAKwAEAAQABAAEAAQABAAEAA0ADQAeAB4AHgAeAB4AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsASwBLAEsASwBLAEsASwBLAEsASwANAA0ADQANAFAABAAEAFAAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAeAA4AUAArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAADQANAB4ADQAEAAQABAAEAB4ABAAEAEsASwBLAEsASwBLAEsASwBLAEsAUAAOAFAADQANAA0AKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAANAA0AHgANAA0AHgAEACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAA0AKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsABAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsABAAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAUAArACsAKwArACsAKwAEACsAKwArACsAKwBQAFAAUABQAFAABAAEACsAKwAEAAQABAAEAAQABAAEACsAKwArAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAAQABABQAFAAUABQAA0ADQANAA0AHgBLAEsASwBLAEsASwBLAEsASwBLAA0ADQArAB4ABABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUAAeAFAAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABAAEAAQADgANAA0AEwATAB4AHgAeAA0ADQANAA0ADQANAA0ADQANAA0ADQANAA0ADQANAFAAUABQAFAABAAEACsAKwAEAA0ADQAeAFAAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKwArACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBcAFwADQANAA0AKgBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAKwArAFAAKwArAFAAUABQAFAAUABQAFAAUAArAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQAKwAEAAQAKwArAAQABAAEAAQAUAAEAFAABAAEAA0ADQANACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABABQAA4AUAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAFAABAAEAAQABAAOAB4ADQANAA0ADQAOAB4ABAArACsAKwArACsAKwArACsAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAA0ADQANAFAADgAOAA4ADQANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAAQABAAEAFAADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAOABMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAArACsAKwAEACsABAAEACsABAAEAAQABAAEAAQABABQAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAaABoAGgAaAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABIAEgAQwBDAEMAUABQAFAAUABDAFAAUABQAEgAQwBIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABDAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAJAAkACQAJAAkACQAJABYAEQArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwANAA0AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAANACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAA0ADQANAB4AHgAeAB4AHgAeAFAAUABQAFAADQAeACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAA0AHgAeACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAARwBHABUARwAJACsAKwArACsAKwArACsAKwArACsAKwAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUQBRAFEAKwArACsAKwArACsAKwArACsAKwArACsAKwBRAFEAUQBRACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAHgAEAAQADQAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQABAAEAAQABAAeAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQAHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAKwArAFAAKwArAFAAUAArACsAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUAArAFAAUABQAFAAUABQAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAHgAeAFAAUABQAFAAUAArAFAAKwArACsAUABQAFAAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeACsAKwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4ABAAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAHgAeAA0ADQANAA0AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArAAQABAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwBQAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArABsAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAB4AHgAeAB4ABAAEAAQABAAEAAQABABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArABYAFgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAGgBQAFAAUAAaAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUAArACsAKwArACsAKwBQACsAKwArACsAUAArAFAAKwBQACsAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUAArAFAAKwBQACsAUAArAFAAUAArAFAAKwArAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAKwBQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8AJQAlACUAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB4AHgAeACUAJQAlAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAlACUAJQAlACUAHgAlACUAJQAlACUAIAAgACAAJQAlACAAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACEAIQAhACEAIQAlACUAIAAgACUAJQAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAIAAlACUAJQAlACAAIAAgACUAIAAgACAAJQAlACUAJQAlACUAJQAgACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAlAB4AJQAeACUAJQAlACUAJQAgACUAJQAlACUAHgAlAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACAAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABcAFwAXABUAFQAVAB4AHgAeAB4AJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAgACUAJQAgACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAIAAgACUAJQAgACAAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACAAIAAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACAAIAAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAA==';\n\nvar LETTER_NUMBER_MODIFIER = 50;\n// Non-tailorable Line Breaking Classes\nvar BK = 1; // Cause a line break (after)\nvar CR$1 = 2; // Cause a line break (after), except between CR and LF\nvar LF$1 = 3; // Cause a line break (after)\nvar CM = 4; // Prohibit a line break between the character and the preceding character\nvar NL = 5; // Cause a line break (after)\nvar WJ = 7; // Prohibit line breaks before and after\nvar ZW = 8; // Provide a break opportunity\nvar GL = 9; // Prohibit line breaks before and after\nvar SP = 10; // Enable indirect line breaks\nvar ZWJ$1 = 11; // Prohibit line breaks within joiner sequences\n// Break Opportunities\nvar B2 = 12; // Provide a line break opportunity before and after the character\nvar BA = 13; // Generally provide a line break opportunity after the character\nvar BB = 14; // Generally provide a line break opportunity before the character\nvar HY = 15; // Provide a line break opportunity after the character, except in numeric context\nvar CB = 16; // Provide a line break opportunity contingent on additional information\n// Characters Prohibiting Certain Breaks\nvar CL = 17; // Prohibit line breaks before\nvar CP = 18; // Prohibit line breaks before\nvar EX = 19; // Prohibit line breaks before\nvar IN = 20; // Allow only indirect line breaks between pairs\nvar NS = 21; // Allow only indirect line breaks before\nvar OP = 22; // Prohibit line breaks after\nvar QU = 23; // Act like they are both opening and closing\n// Numeric Context\nvar IS = 24; // Prevent breaks after any and before numeric\nvar NU = 25; // Form numeric expressions for line breaking purposes\nvar PO = 26; // Do not break following a numeric expression\nvar PR = 27; // Do not break in front of a numeric expression\nvar SY = 28; // Prevent a break before; and allow a break after\n// Other Characters\nvar AI = 29; // Act like AL when the resolvedEAW is N; otherwise; act as ID\nvar AL = 30; // Are alphabetic characters or symbols that are used with alphabetic characters\nvar CJ = 31; // Treat as NS or ID for strict or normal breaking.\nvar EB = 32; // Do not break from following Emoji Modifier\nvar EM = 33; // Do not break from preceding Emoji Base\nvar H2 = 34; // Form Korean syllable blocks\nvar H3 = 35; // Form Korean syllable blocks\nvar HL = 36; // Do not break around a following hyphen; otherwise act as Alphabetic\nvar ID = 37; // Break before or after; except in some numeric context\nvar JL = 38; // Form Korean syllable blocks\nvar JV = 39; // Form Korean syllable blocks\nvar JT = 40; // Form Korean syllable blocks\nvar RI$1 = 41; // Keep pairs together. For pairs; break before and after other classes\nvar SA = 42; // Provide a line break opportunity contingent on additional, language-specific context analysis\nvar XX = 43; // Have as yet unknown line breaking behavior or unassigned code positions\nvar ea_OP = [0x2329, 0xff08];\nvar BREAK_MANDATORY = '!';\nvar BREAK_NOT_ALLOWED$1 = '×';\nvar BREAK_ALLOWED$1 = '÷';\nvar UnicodeTrie$1 = createTrieFromBase64$1(base64$1);\nvar ALPHABETICS = [AL, HL];\nvar HARD_LINE_BREAKS = [BK, CR$1, LF$1, NL];\nvar SPACE$1 = [SP, ZW];\nvar PREFIX_POSTFIX = [PR, PO];\nvar LINE_BREAKS = HARD_LINE_BREAKS.concat(SPACE$1);\nvar KOREAN_SYLLABLE_BLOCK = [JL, JV, JT, H2, H3];\nvar HYPHEN = [HY, BA];\nvar codePointsToCharacterClasses = function (codePoints, lineBreak) {\n if (lineBreak === void 0) { lineBreak = 'strict'; }\n var types = [];\n var indices = [];\n var categories = [];\n codePoints.forEach(function (codePoint, index) {\n var classType = UnicodeTrie$1.get(codePoint);\n if (classType > LETTER_NUMBER_MODIFIER) {\n categories.push(true);\n classType -= LETTER_NUMBER_MODIFIER;\n }\n else {\n categories.push(false);\n }\n if (['normal', 'auto', 'loose'].indexOf(lineBreak) !== -1) {\n // U+2010, – U+2013, 〜 U+301C, ゠ U+30A0\n if ([0x2010, 0x2013, 0x301c, 0x30a0].indexOf(codePoint) !== -1) {\n indices.push(index);\n return types.push(CB);\n }\n }\n if (classType === CM || classType === ZWJ$1) {\n // LB10 Treat any remaining combining mark or ZWJ as AL.\n if (index === 0) {\n indices.push(index);\n return types.push(AL);\n }\n // LB9 Do not break a combining character sequence; treat it as if it has the line breaking class of\n // the base character in all of the following rules. Treat ZWJ as if it were CM.\n var prev = types[index - 1];\n if (LINE_BREAKS.indexOf(prev) === -1) {\n indices.push(indices[index - 1]);\n return types.push(prev);\n }\n indices.push(index);\n return types.push(AL);\n }\n indices.push(index);\n if (classType === CJ) {\n return types.push(lineBreak === 'strict' ? NS : ID);\n }\n if (classType === SA) {\n return types.push(AL);\n }\n if (classType === AI) {\n return types.push(AL);\n }\n // For supplementary characters, a useful default is to treat characters in the range 10000..1FFFD as AL\n // and characters in the ranges 20000..2FFFD and 30000..3FFFD as ID, until the implementation can be revised\n // to take into account the actual line breaking properties for these characters.\n if (classType === XX) {\n if ((codePoint >= 0x20000 && codePoint <= 0x2fffd) || (codePoint >= 0x30000 && codePoint <= 0x3fffd)) {\n return types.push(ID);\n }\n else {\n return types.push(AL);\n }\n }\n types.push(classType);\n });\n return [indices, types, categories];\n};\nvar isAdjacentWithSpaceIgnored = function (a, b, currentIndex, classTypes) {\n var current = classTypes[currentIndex];\n if (Array.isArray(a) ? a.indexOf(current) !== -1 : a === current) {\n var i = currentIndex;\n while (i <= classTypes.length) {\n i++;\n var next = classTypes[i];\n if (next === b) {\n return true;\n }\n if (next !== SP) {\n break;\n }\n }\n }\n if (current === SP) {\n var i = currentIndex;\n while (i > 0) {\n i--;\n var prev = classTypes[i];\n if (Array.isArray(a) ? a.indexOf(prev) !== -1 : a === prev) {\n var n = currentIndex;\n while (n <= classTypes.length) {\n n++;\n var next = classTypes[n];\n if (next === b) {\n return true;\n }\n if (next !== SP) {\n break;\n }\n }\n }\n if (prev !== SP) {\n break;\n }\n }\n }\n return false;\n};\nvar previousNonSpaceClassType = function (currentIndex, classTypes) {\n var i = currentIndex;\n while (i >= 0) {\n var type = classTypes[i];\n if (type === SP) {\n i--;\n }\n else {\n return type;\n }\n }\n return 0;\n};\nvar _lineBreakAtIndex = function (codePoints, classTypes, indicies, index, forbiddenBreaks) {\n if (indicies[index] === 0) {\n return BREAK_NOT_ALLOWED$1;\n }\n var currentIndex = index - 1;\n if (Array.isArray(forbiddenBreaks) && forbiddenBreaks[currentIndex] === true) {\n return BREAK_NOT_ALLOWED$1;\n }\n var beforeIndex = currentIndex - 1;\n var afterIndex = currentIndex + 1;\n var current = classTypes[currentIndex];\n // LB4 Always break after hard line breaks.\n // LB5 Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks.\n var before = beforeIndex >= 0 ? classTypes[beforeIndex] : 0;\n var next = classTypes[afterIndex];\n if (current === CR$1 && next === LF$1) {\n return BREAK_NOT_ALLOWED$1;\n }\n if (HARD_LINE_BREAKS.indexOf(current) !== -1) {\n return BREAK_MANDATORY;\n }\n // LB6 Do not break before hard line breaks.\n if (HARD_LINE_BREAKS.indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB7 Do not break before spaces or zero width space.\n if (SPACE$1.indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB8 Break before any character following a zero-width space, even if one or more spaces intervene.\n if (previousNonSpaceClassType(currentIndex, classTypes) === ZW) {\n return BREAK_ALLOWED$1;\n }\n // LB8a Do not break after a zero width joiner.\n if (UnicodeTrie$1.get(codePoints[currentIndex]) === ZWJ$1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // zwj emojis\n if ((current === EB || current === EM) && UnicodeTrie$1.get(codePoints[afterIndex]) === ZWJ$1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB11 Do not break before or after Word joiner and related characters.\n if (current === WJ || next === WJ) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB12 Do not break after NBSP and related characters.\n if (current === GL) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB12a Do not break before NBSP and related characters, except after spaces and hyphens.\n if ([SP, BA, HY].indexOf(current) === -1 && next === GL) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB13 Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces.\n if ([CL, CP, EX, IS, SY].indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB14 Do not break after ‘[’, even after spaces.\n if (previousNonSpaceClassType(currentIndex, classTypes) === OP) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB15 Do not break within ‘”[’, even with intervening spaces.\n if (isAdjacentWithSpaceIgnored(QU, OP, currentIndex, classTypes)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB16 Do not break between closing punctuation and a nonstarter (lb=NS), even with intervening spaces.\n if (isAdjacentWithSpaceIgnored([CL, CP], NS, currentIndex, classTypes)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB17 Do not break within ‘——’, even with intervening spaces.\n if (isAdjacentWithSpaceIgnored(B2, B2, currentIndex, classTypes)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB18 Break after spaces.\n if (current === SP) {\n return BREAK_ALLOWED$1;\n }\n // LB19 Do not break before or after quotation marks, such as ‘ ” ’.\n if (current === QU || next === QU) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB20 Break before and after unresolved CB.\n if (next === CB || current === CB) {\n return BREAK_ALLOWED$1;\n }\n // LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents.\n if ([BA, HY, NS].indexOf(next) !== -1 || current === BB) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB21a Don't break after Hebrew + Hyphen.\n if (before === HL && HYPHEN.indexOf(current) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB21b Don’t break between Solidus and Hebrew letters.\n if (current === SY && next === HL) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB22 Do not break before ellipsis.\n if (next === IN) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB23 Do not break between digits and letters.\n if ((ALPHABETICS.indexOf(next) !== -1 && current === NU) || (ALPHABETICS.indexOf(current) !== -1 && next === NU)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB23a Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes.\n if ((current === PR && [ID, EB, EM].indexOf(next) !== -1) ||\n ([ID, EB, EM].indexOf(current) !== -1 && next === PO)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB24 Do not break between numeric prefix/postfix and letters, or between letters and prefix/postfix.\n if ((ALPHABETICS.indexOf(current) !== -1 && PREFIX_POSTFIX.indexOf(next) !== -1) ||\n (PREFIX_POSTFIX.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB25 Do not break between the following pairs of classes relevant to numbers:\n if (\n // (PR | PO) × ( OP | HY )? NU\n ([PR, PO].indexOf(current) !== -1 &&\n (next === NU || ([OP, HY].indexOf(next) !== -1 && classTypes[afterIndex + 1] === NU))) ||\n // ( OP | HY ) × NU\n ([OP, HY].indexOf(current) !== -1 && next === NU) ||\n // NU ×\t(NU | SY | IS)\n (current === NU && [NU, SY, IS].indexOf(next) !== -1)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // NU (NU | SY | IS)* × (NU | SY | IS | CL | CP)\n if ([NU, SY, IS, CL, CP].indexOf(next) !== -1) {\n var prevIndex = currentIndex;\n while (prevIndex >= 0) {\n var type = classTypes[prevIndex];\n if (type === NU) {\n return BREAK_NOT_ALLOWED$1;\n }\n else if ([SY, IS].indexOf(type) !== -1) {\n prevIndex--;\n }\n else {\n break;\n }\n }\n }\n // NU (NU | SY | IS)* (CL | CP)? × (PO | PR))\n if ([PR, PO].indexOf(next) !== -1) {\n var prevIndex = [CL, CP].indexOf(current) !== -1 ? beforeIndex : currentIndex;\n while (prevIndex >= 0) {\n var type = classTypes[prevIndex];\n if (type === NU) {\n return BREAK_NOT_ALLOWED$1;\n }\n else if ([SY, IS].indexOf(type) !== -1) {\n prevIndex--;\n }\n else {\n break;\n }\n }\n }\n // LB26 Do not break a Korean syllable.\n if ((JL === current && [JL, JV, H2, H3].indexOf(next) !== -1) ||\n ([JV, H2].indexOf(current) !== -1 && [JV, JT].indexOf(next) !== -1) ||\n ([JT, H3].indexOf(current) !== -1 && next === JT)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB27 Treat a Korean Syllable Block the same as ID.\n if ((KOREAN_SYLLABLE_BLOCK.indexOf(current) !== -1 && [IN, PO].indexOf(next) !== -1) ||\n (KOREAN_SYLLABLE_BLOCK.indexOf(next) !== -1 && current === PR)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB28 Do not break between alphabetics (“at”).\n if (ALPHABETICS.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB29 Do not break between numeric punctuation and alphabetics (“e.g.”).\n if (current === IS && ALPHABETICS.indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB30 Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses.\n if ((ALPHABETICS.concat(NU).indexOf(current) !== -1 &&\n next === OP &&\n ea_OP.indexOf(codePoints[afterIndex]) === -1) ||\n (ALPHABETICS.concat(NU).indexOf(next) !== -1 && current === CP)) {\n return BREAK_NOT_ALLOWED$1;\n }\n // LB30a Break between two regional indicator symbols if and only if there are an even number of regional\n // indicators preceding the position of the break.\n if (current === RI$1 && next === RI$1) {\n var i = indicies[currentIndex];\n var count = 1;\n while (i > 0) {\n i--;\n if (classTypes[i] === RI$1) {\n count++;\n }\n else {\n break;\n }\n }\n if (count % 2 !== 0) {\n return BREAK_NOT_ALLOWED$1;\n }\n }\n // LB30b Do not break between an emoji base and an emoji modifier.\n if (current === EB && next === EM) {\n return BREAK_NOT_ALLOWED$1;\n }\n return BREAK_ALLOWED$1;\n};\nvar cssFormattedClasses = function (codePoints, options) {\n if (!options) {\n options = { lineBreak: 'normal', wordBreak: 'normal' };\n }\n var _a = codePointsToCharacterClasses(codePoints, options.lineBreak), indicies = _a[0], classTypes = _a[1], isLetterNumber = _a[2];\n if (options.wordBreak === 'break-all' || options.wordBreak === 'break-word') {\n classTypes = classTypes.map(function (type) { return ([NU, AL, SA].indexOf(type) !== -1 ? ID : type); });\n }\n var forbiddenBreakpoints = options.wordBreak === 'keep-all'\n ? isLetterNumber.map(function (letterNumber, i) {\n return letterNumber && codePoints[i] >= 0x4e00 && codePoints[i] <= 0x9fff;\n })\n : undefined;\n return [indicies, classTypes, forbiddenBreakpoints];\n};\nvar Break = /** @class */ (function () {\n function Break(codePoints, lineBreak, start, end) {\n this.codePoints = codePoints;\n this.required = lineBreak === BREAK_MANDATORY;\n this.start = start;\n this.end = end;\n }\n Break.prototype.slice = function () {\n return fromCodePoint$1.apply(void 0, this.codePoints.slice(this.start, this.end));\n };\n return Break;\n}());\nvar LineBreaker = function (str, options) {\n var codePoints = toCodePoints$1(str);\n var _a = cssFormattedClasses(codePoints, options), indicies = _a[0], classTypes = _a[1], forbiddenBreakpoints = _a[2];\n var length = codePoints.length;\n var lastEnd = 0;\n var nextIndex = 0;\n return {\n next: function () {\n if (nextIndex >= length) {\n return { done: true, value: null };\n }\n var lineBreak = BREAK_NOT_ALLOWED$1;\n while (nextIndex < length &&\n (lineBreak = _lineBreakAtIndex(codePoints, classTypes, indicies, ++nextIndex, forbiddenBreakpoints)) ===\n BREAK_NOT_ALLOWED$1) { }\n if (lineBreak !== BREAK_NOT_ALLOWED$1 || nextIndex === length) {\n var value = new Break(codePoints, lineBreak, lastEnd, nextIndex);\n lastEnd = nextIndex;\n return { value: value, done: false };\n }\n return { done: true, value: null };\n },\n };\n};\n\n// https://www.w3.org/TR/css-syntax-3\nvar FLAG_UNRESTRICTED = 1 << 0;\nvar FLAG_ID = 1 << 1;\nvar FLAG_INTEGER = 1 << 2;\nvar FLAG_NUMBER = 1 << 3;\nvar LINE_FEED = 0x000a;\nvar SOLIDUS = 0x002f;\nvar REVERSE_SOLIDUS = 0x005c;\nvar CHARACTER_TABULATION = 0x0009;\nvar SPACE = 0x0020;\nvar QUOTATION_MARK = 0x0022;\nvar EQUALS_SIGN = 0x003d;\nvar NUMBER_SIGN = 0x0023;\nvar DOLLAR_SIGN = 0x0024;\nvar PERCENTAGE_SIGN = 0x0025;\nvar APOSTROPHE = 0x0027;\nvar LEFT_PARENTHESIS = 0x0028;\nvar RIGHT_PARENTHESIS = 0x0029;\nvar LOW_LINE = 0x005f;\nvar HYPHEN_MINUS = 0x002d;\nvar EXCLAMATION_MARK = 0x0021;\nvar LESS_THAN_SIGN = 0x003c;\nvar GREATER_THAN_SIGN = 0x003e;\nvar COMMERCIAL_AT = 0x0040;\nvar LEFT_SQUARE_BRACKET = 0x005b;\nvar RIGHT_SQUARE_BRACKET = 0x005d;\nvar CIRCUMFLEX_ACCENT = 0x003d;\nvar LEFT_CURLY_BRACKET = 0x007b;\nvar QUESTION_MARK = 0x003f;\nvar RIGHT_CURLY_BRACKET = 0x007d;\nvar VERTICAL_LINE = 0x007c;\nvar TILDE = 0x007e;\nvar CONTROL = 0x0080;\nvar REPLACEMENT_CHARACTER = 0xfffd;\nvar ASTERISK = 0x002a;\nvar PLUS_SIGN = 0x002b;\nvar COMMA = 0x002c;\nvar COLON = 0x003a;\nvar SEMICOLON = 0x003b;\nvar FULL_STOP = 0x002e;\nvar NULL = 0x0000;\nvar BACKSPACE = 0x0008;\nvar LINE_TABULATION = 0x000b;\nvar SHIFT_OUT = 0x000e;\nvar INFORMATION_SEPARATOR_ONE = 0x001f;\nvar DELETE = 0x007f;\nvar EOF = -1;\nvar ZERO = 0x0030;\nvar a = 0x0061;\nvar e = 0x0065;\nvar f = 0x0066;\nvar u = 0x0075;\nvar z = 0x007a;\nvar A = 0x0041;\nvar E = 0x0045;\nvar F = 0x0046;\nvar U = 0x0055;\nvar Z = 0x005a;\nvar isDigit = function (codePoint) { return codePoint >= ZERO && codePoint <= 0x0039; };\nvar isSurrogateCodePoint = function (codePoint) { return codePoint >= 0xd800 && codePoint <= 0xdfff; };\nvar isHex = function (codePoint) {\n return isDigit(codePoint) || (codePoint >= A && codePoint <= F) || (codePoint >= a && codePoint <= f);\n};\nvar isLowerCaseLetter = function (codePoint) { return codePoint >= a && codePoint <= z; };\nvar isUpperCaseLetter = function (codePoint) { return codePoint >= A && codePoint <= Z; };\nvar isLetter = function (codePoint) { return isLowerCaseLetter(codePoint) || isUpperCaseLetter(codePoint); };\nvar isNonASCIICodePoint = function (codePoint) { return codePoint >= CONTROL; };\nvar isWhiteSpace = function (codePoint) {\n return codePoint === LINE_FEED || codePoint === CHARACTER_TABULATION || codePoint === SPACE;\n};\nvar isNameStartCodePoint = function (codePoint) {\n return isLetter(codePoint) || isNonASCIICodePoint(codePoint) || codePoint === LOW_LINE;\n};\nvar isNameCodePoint = function (codePoint) {\n return isNameStartCodePoint(codePoint) || isDigit(codePoint) || codePoint === HYPHEN_MINUS;\n};\nvar isNonPrintableCodePoint = function (codePoint) {\n return ((codePoint >= NULL && codePoint <= BACKSPACE) ||\n codePoint === LINE_TABULATION ||\n (codePoint >= SHIFT_OUT && codePoint <= INFORMATION_SEPARATOR_ONE) ||\n codePoint === DELETE);\n};\nvar isValidEscape = function (c1, c2) {\n if (c1 !== REVERSE_SOLIDUS) {\n return false;\n }\n return c2 !== LINE_FEED;\n};\nvar isIdentifierStart = function (c1, c2, c3) {\n if (c1 === HYPHEN_MINUS) {\n return isNameStartCodePoint(c2) || isValidEscape(c2, c3);\n }\n else if (isNameStartCodePoint(c1)) {\n return true;\n }\n else if (c1 === REVERSE_SOLIDUS && isValidEscape(c1, c2)) {\n return true;\n }\n return false;\n};\nvar isNumberStart = function (c1, c2, c3) {\n if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {\n if (isDigit(c2)) {\n return true;\n }\n return c2 === FULL_STOP && isDigit(c3);\n }\n if (c1 === FULL_STOP) {\n return isDigit(c2);\n }\n return isDigit(c1);\n};\nvar stringToNumber = function (codePoints) {\n var c = 0;\n var sign = 1;\n if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {\n if (codePoints[c] === HYPHEN_MINUS) {\n sign = -1;\n }\n c++;\n }\n var integers = [];\n while (isDigit(codePoints[c])) {\n integers.push(codePoints[c++]);\n }\n var int = integers.length ? parseInt(fromCodePoint$1.apply(void 0, integers), 10) : 0;\n if (codePoints[c] === FULL_STOP) {\n c++;\n }\n var fraction = [];\n while (isDigit(codePoints[c])) {\n fraction.push(codePoints[c++]);\n }\n var fracd = fraction.length;\n var frac = fracd ? parseInt(fromCodePoint$1.apply(void 0, fraction), 10) : 0;\n if (codePoints[c] === E || codePoints[c] === e) {\n c++;\n }\n var expsign = 1;\n if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {\n if (codePoints[c] === HYPHEN_MINUS) {\n expsign = -1;\n }\n c++;\n }\n var exponent = [];\n while (isDigit(codePoints[c])) {\n exponent.push(codePoints[c++]);\n }\n var exp = exponent.length ? parseInt(fromCodePoint$1.apply(void 0, exponent), 10) : 0;\n return sign * (int + frac * Math.pow(10, -fracd)) * Math.pow(10, expsign * exp);\n};\nvar LEFT_PARENTHESIS_TOKEN = {\n type: 2 /* LEFT_PARENTHESIS_TOKEN */\n};\nvar RIGHT_PARENTHESIS_TOKEN = {\n type: 3 /* RIGHT_PARENTHESIS_TOKEN */\n};\nvar COMMA_TOKEN = { type: 4 /* COMMA_TOKEN */ };\nvar SUFFIX_MATCH_TOKEN = { type: 13 /* SUFFIX_MATCH_TOKEN */ };\nvar PREFIX_MATCH_TOKEN = { type: 8 /* PREFIX_MATCH_TOKEN */ };\nvar COLUMN_TOKEN = { type: 21 /* COLUMN_TOKEN */ };\nvar DASH_MATCH_TOKEN = { type: 9 /* DASH_MATCH_TOKEN */ };\nvar INCLUDE_MATCH_TOKEN = { type: 10 /* INCLUDE_MATCH_TOKEN */ };\nvar LEFT_CURLY_BRACKET_TOKEN = {\n type: 11 /* LEFT_CURLY_BRACKET_TOKEN */\n};\nvar RIGHT_CURLY_BRACKET_TOKEN = {\n type: 12 /* RIGHT_CURLY_BRACKET_TOKEN */\n};\nvar SUBSTRING_MATCH_TOKEN = { type: 14 /* SUBSTRING_MATCH_TOKEN */ };\nvar BAD_URL_TOKEN = { type: 23 /* BAD_URL_TOKEN */ };\nvar BAD_STRING_TOKEN = { type: 1 /* BAD_STRING_TOKEN */ };\nvar CDO_TOKEN = { type: 25 /* CDO_TOKEN */ };\nvar CDC_TOKEN = { type: 24 /* CDC_TOKEN */ };\nvar COLON_TOKEN = { type: 26 /* COLON_TOKEN */ };\nvar SEMICOLON_TOKEN = { type: 27 /* SEMICOLON_TOKEN */ };\nvar LEFT_SQUARE_BRACKET_TOKEN = {\n type: 28 /* LEFT_SQUARE_BRACKET_TOKEN */\n};\nvar RIGHT_SQUARE_BRACKET_TOKEN = {\n type: 29 /* RIGHT_SQUARE_BRACKET_TOKEN */\n};\nvar WHITESPACE_TOKEN = { type: 31 /* WHITESPACE_TOKEN */ };\nvar EOF_TOKEN = { type: 32 /* EOF_TOKEN */ };\nvar Tokenizer = /** @class */ (function () {\n function Tokenizer() {\n this._value = [];\n }\n Tokenizer.prototype.write = function (chunk) {\n this._value = this._value.concat(toCodePoints$1(chunk));\n };\n Tokenizer.prototype.read = function () {\n var tokens = [];\n var token = this.consumeToken();\n while (token !== EOF_TOKEN) {\n tokens.push(token);\n token = this.consumeToken();\n }\n return tokens;\n };\n Tokenizer.prototype.consumeToken = function () {\n var codePoint = this.consumeCodePoint();\n switch (codePoint) {\n case QUOTATION_MARK:\n return this.consumeStringToken(QUOTATION_MARK);\n case NUMBER_SIGN:\n var c1 = this.peekCodePoint(0);\n var c2 = this.peekCodePoint(1);\n var c3 = this.peekCodePoint(2);\n if (isNameCodePoint(c1) || isValidEscape(c2, c3)) {\n var flags = isIdentifierStart(c1, c2, c3) ? FLAG_ID : FLAG_UNRESTRICTED;\n var value = this.consumeName();\n return { type: 5 /* HASH_TOKEN */, value: value, flags: flags };\n }\n break;\n case DOLLAR_SIGN:\n if (this.peekCodePoint(0) === EQUALS_SIGN) {\n this.consumeCodePoint();\n return SUFFIX_MATCH_TOKEN;\n }\n break;\n case APOSTROPHE:\n return this.consumeStringToken(APOSTROPHE);\n case LEFT_PARENTHESIS:\n return LEFT_PARENTHESIS_TOKEN;\n case RIGHT_PARENTHESIS:\n return RIGHT_PARENTHESIS_TOKEN;\n case ASTERISK:\n if (this.peekCodePoint(0) === EQUALS_SIGN) {\n this.consumeCodePoint();\n return SUBSTRING_MATCH_TOKEN;\n }\n break;\n case PLUS_SIGN:\n if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeNumericToken();\n }\n break;\n case COMMA:\n return COMMA_TOKEN;\n case HYPHEN_MINUS:\n var e1 = codePoint;\n var e2 = this.peekCodePoint(0);\n var e3 = this.peekCodePoint(1);\n if (isNumberStart(e1, e2, e3)) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeNumericToken();\n }\n if (isIdentifierStart(e1, e2, e3)) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeIdentLikeToken();\n }\n if (e2 === HYPHEN_MINUS && e3 === GREATER_THAN_SIGN) {\n this.consumeCodePoint();\n this.consumeCodePoint();\n return CDC_TOKEN;\n }\n break;\n case FULL_STOP:\n if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeNumericToken();\n }\n break;\n case SOLIDUS:\n if (this.peekCodePoint(0) === ASTERISK) {\n this.consumeCodePoint();\n while (true) {\n var c = this.consumeCodePoint();\n if (c === ASTERISK) {\n c = this.consumeCodePoint();\n if (c === SOLIDUS) {\n return this.consumeToken();\n }\n }\n if (c === EOF) {\n return this.consumeToken();\n }\n }\n }\n break;\n case COLON:\n return COLON_TOKEN;\n case SEMICOLON:\n return SEMICOLON_TOKEN;\n case LESS_THAN_SIGN:\n if (this.peekCodePoint(0) === EXCLAMATION_MARK &&\n this.peekCodePoint(1) === HYPHEN_MINUS &&\n this.peekCodePoint(2) === HYPHEN_MINUS) {\n this.consumeCodePoint();\n this.consumeCodePoint();\n return CDO_TOKEN;\n }\n break;\n case COMMERCIAL_AT:\n var a1 = this.peekCodePoint(0);\n var a2 = this.peekCodePoint(1);\n var a3 = this.peekCodePoint(2);\n if (isIdentifierStart(a1, a2, a3)) {\n var value = this.consumeName();\n return { type: 7 /* AT_KEYWORD_TOKEN */, value: value };\n }\n break;\n case LEFT_SQUARE_BRACKET:\n return LEFT_SQUARE_BRACKET_TOKEN;\n case REVERSE_SOLIDUS:\n if (isValidEscape(codePoint, this.peekCodePoint(0))) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeIdentLikeToken();\n }\n break;\n case RIGHT_SQUARE_BRACKET:\n return RIGHT_SQUARE_BRACKET_TOKEN;\n case CIRCUMFLEX_ACCENT:\n if (this.peekCodePoint(0) === EQUALS_SIGN) {\n this.consumeCodePoint();\n return PREFIX_MATCH_TOKEN;\n }\n break;\n case LEFT_CURLY_BRACKET:\n return LEFT_CURLY_BRACKET_TOKEN;\n case RIGHT_CURLY_BRACKET:\n return RIGHT_CURLY_BRACKET_TOKEN;\n case u:\n case U:\n var u1 = this.peekCodePoint(0);\n var u2 = this.peekCodePoint(1);\n if (u1 === PLUS_SIGN && (isHex(u2) || u2 === QUESTION_MARK)) {\n this.consumeCodePoint();\n this.consumeUnicodeRangeToken();\n }\n this.reconsumeCodePoint(codePoint);\n return this.consumeIdentLikeToken();\n case VERTICAL_LINE:\n if (this.peekCodePoint(0) === EQUALS_SIGN) {\n this.consumeCodePoint();\n return DASH_MATCH_TOKEN;\n }\n if (this.peekCodePoint(0) === VERTICAL_LINE) {\n this.consumeCodePoint();\n return COLUMN_TOKEN;\n }\n break;\n case TILDE:\n if (this.peekCodePoint(0) === EQUALS_SIGN) {\n this.consumeCodePoint();\n return INCLUDE_MATCH_TOKEN;\n }\n break;\n case EOF:\n return EOF_TOKEN;\n }\n if (isWhiteSpace(codePoint)) {\n this.consumeWhiteSpace();\n return WHITESPACE_TOKEN;\n }\n if (isDigit(codePoint)) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeNumericToken();\n }\n if (isNameStartCodePoint(codePoint)) {\n this.reconsumeCodePoint(codePoint);\n return this.consumeIdentLikeToken();\n }\n return { type: 6 /* DELIM_TOKEN */, value: fromCodePoint$1(codePoint) };\n };\n Tokenizer.prototype.consumeCodePoint = function () {\n var value = this._value.shift();\n return typeof value === 'undefined' ? -1 : value;\n };\n Tokenizer.prototype.reconsumeCodePoint = function (codePoint) {\n this._value.unshift(codePoint);\n };\n Tokenizer.prototype.peekCodePoint = function (delta) {\n if (delta >= this._value.length) {\n return -1;\n }\n return this._value[delta];\n };\n Tokenizer.prototype.consumeUnicodeRangeToken = function () {\n var digits = [];\n var codePoint = this.consumeCodePoint();\n while (isHex(codePoint) && digits.length < 6) {\n digits.push(codePoint);\n codePoint = this.consumeCodePoint();\n }\n var questionMarks = false;\n while (codePoint === QUESTION_MARK && digits.length < 6) {\n digits.push(codePoint);\n codePoint = this.consumeCodePoint();\n questionMarks = true;\n }\n if (questionMarks) {\n var start_1 = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? ZERO : digit); })), 16);\n var end = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? F : digit); })), 16);\n return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start_1, end: end };\n }\n var start = parseInt(fromCodePoint$1.apply(void 0, digits), 16);\n if (this.peekCodePoint(0) === HYPHEN_MINUS && isHex(this.peekCodePoint(1))) {\n this.consumeCodePoint();\n codePoint = this.consumeCodePoint();\n var endDigits = [];\n while (isHex(codePoint) && endDigits.length < 6) {\n endDigits.push(codePoint);\n codePoint = this.consumeCodePoint();\n }\n var end = parseInt(fromCodePoint$1.apply(void 0, endDigits), 16);\n return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: end };\n }\n else {\n return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: start };\n }\n };\n Tokenizer.prototype.consumeIdentLikeToken = function () {\n var value = this.consumeName();\n if (value.toLowerCase() === 'url' && this.peekCodePoint(0) === LEFT_PARENTHESIS) {\n this.consumeCodePoint();\n return this.consumeUrlToken();\n }\n else if (this.peekCodePoint(0) === LEFT_PARENTHESIS) {\n this.consumeCodePoint();\n return { type: 19 /* FUNCTION_TOKEN */, value: value };\n }\n return { type: 20 /* IDENT_TOKEN */, value: value };\n };\n Tokenizer.prototype.consumeUrlToken = function () {\n var value = [];\n this.consumeWhiteSpace();\n if (this.peekCodePoint(0) === EOF) {\n return { type: 22 /* URL_TOKEN */, value: '' };\n }\n var next = this.peekCodePoint(0);\n if (next === APOSTROPHE || next === QUOTATION_MARK) {\n var stringToken = this.consumeStringToken(this.consumeCodePoint());\n if (stringToken.type === 0 /* STRING_TOKEN */) {\n this.consumeWhiteSpace();\n if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {\n this.consumeCodePoint();\n return { type: 22 /* URL_TOKEN */, value: stringToken.value };\n }\n }\n this.consumeBadUrlRemnants();\n return BAD_URL_TOKEN;\n }\n while (true) {\n var codePoint = this.consumeCodePoint();\n if (codePoint === EOF || codePoint === RIGHT_PARENTHESIS) {\n return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) };\n }\n else if (isWhiteSpace(codePoint)) {\n this.consumeWhiteSpace();\n if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {\n this.consumeCodePoint();\n return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) };\n }\n this.consumeBadUrlRemnants();\n return BAD_URL_TOKEN;\n }\n else if (codePoint === QUOTATION_MARK ||\n codePoint === APOSTROPHE ||\n codePoint === LEFT_PARENTHESIS ||\n isNonPrintableCodePoint(codePoint)) {\n this.consumeBadUrlRemnants();\n return BAD_URL_TOKEN;\n }\n else if (codePoint === REVERSE_SOLIDUS) {\n if (isValidEscape(codePoint, this.peekCodePoint(0))) {\n value.push(this.consumeEscapedCodePoint());\n }\n else {\n this.consumeBadUrlRemnants();\n return BAD_URL_TOKEN;\n }\n }\n else {\n value.push(codePoint);\n }\n }\n };\n Tokenizer.prototype.consumeWhiteSpace = function () {\n while (isWhiteSpace(this.peekCodePoint(0))) {\n this.consumeCodePoint();\n }\n };\n Tokenizer.prototype.consumeBadUrlRemnants = function () {\n while (true) {\n var codePoint = this.consumeCodePoint();\n if (codePoint === RIGHT_PARENTHESIS || codePoint === EOF) {\n return;\n }\n if (isValidEscape(codePoint, this.peekCodePoint(0))) {\n this.consumeEscapedCodePoint();\n }\n }\n };\n Tokenizer.prototype.consumeStringSlice = function (count) {\n var SLICE_STACK_SIZE = 50000;\n var value = '';\n while (count > 0) {\n var amount = Math.min(SLICE_STACK_SIZE, count);\n value += fromCodePoint$1.apply(void 0, this._value.splice(0, amount));\n count -= amount;\n }\n this._value.shift();\n return value;\n };\n Tokenizer.prototype.consumeStringToken = function (endingCodePoint) {\n var value = '';\n var i = 0;\n do {\n var codePoint = this._value[i];\n if (codePoint === EOF || codePoint === undefined || codePoint === endingCodePoint) {\n value += this.consumeStringSlice(i);\n return { type: 0 /* STRING_TOKEN */, value: value };\n }\n if (codePoint === LINE_FEED) {\n this._value.splice(0, i);\n return BAD_STRING_TOKEN;\n }\n if (codePoint === REVERSE_SOLIDUS) {\n var next = this._value[i + 1];\n if (next !== EOF && next !== undefined) {\n if (next === LINE_FEED) {\n value += this.consumeStringSlice(i);\n i = -1;\n this._value.shift();\n }\n else if (isValidEscape(codePoint, next)) {\n value += this.consumeStringSlice(i);\n value += fromCodePoint$1(this.consumeEscapedCodePoint());\n i = -1;\n }\n }\n }\n i++;\n } while (true);\n };\n Tokenizer.prototype.consumeNumber = function () {\n var repr = [];\n var type = FLAG_INTEGER;\n var c1 = this.peekCodePoint(0);\n if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {\n repr.push(this.consumeCodePoint());\n }\n while (isDigit(this.peekCodePoint(0))) {\n repr.push(this.consumeCodePoint());\n }\n c1 = this.peekCodePoint(0);\n var c2 = this.peekCodePoint(1);\n if (c1 === FULL_STOP && isDigit(c2)) {\n repr.push(this.consumeCodePoint(), this.consumeCodePoint());\n type = FLAG_NUMBER;\n while (isDigit(this.peekCodePoint(0))) {\n repr.push(this.consumeCodePoint());\n }\n }\n c1 = this.peekCodePoint(0);\n c2 = this.peekCodePoint(1);\n var c3 = this.peekCodePoint(2);\n if ((c1 === E || c1 === e) && (((c2 === PLUS_SIGN || c2 === HYPHEN_MINUS) && isDigit(c3)) || isDigit(c2))) {\n repr.push(this.consumeCodePoint(), this.consumeCodePoint());\n type = FLAG_NUMBER;\n while (isDigit(this.peekCodePoint(0))) {\n repr.push(this.consumeCodePoint());\n }\n }\n return [stringToNumber(repr), type];\n };\n Tokenizer.prototype.consumeNumericToken = function () {\n var _a = this.consumeNumber(), number = _a[0], flags = _a[1];\n var c1 = this.peekCodePoint(0);\n var c2 = this.peekCodePoint(1);\n var c3 = this.peekCodePoint(2);\n if (isIdentifierStart(c1, c2, c3)) {\n var unit = this.consumeName();\n return { type: 15 /* DIMENSION_TOKEN */, number: number, flags: flags, unit: unit };\n }\n if (c1 === PERCENTAGE_SIGN) {\n this.consumeCodePoint();\n return { type: 16 /* PERCENTAGE_TOKEN */, number: number, flags: flags };\n }\n return { type: 17 /* NUMBER_TOKEN */, number: number, flags: flags };\n };\n Tokenizer.prototype.consumeEscapedCodePoint = function () {\n var codePoint = this.consumeCodePoint();\n if (isHex(codePoint)) {\n var hex = fromCodePoint$1(codePoint);\n while (isHex(this.peekCodePoint(0)) && hex.length < 6) {\n hex += fromCodePoint$1(this.consumeCodePoint());\n }\n if (isWhiteSpace(this.peekCodePoint(0))) {\n this.consumeCodePoint();\n }\n var hexCodePoint = parseInt(hex, 16);\n if (hexCodePoint === 0 || isSurrogateCodePoint(hexCodePoint) || hexCodePoint > 0x10ffff) {\n return REPLACEMENT_CHARACTER;\n }\n return hexCodePoint;\n }\n if (codePoint === EOF) {\n return REPLACEMENT_CHARACTER;\n }\n return codePoint;\n };\n Tokenizer.prototype.consumeName = function () {\n var result = '';\n while (true) {\n var codePoint = this.consumeCodePoint();\n if (isNameCodePoint(codePoint)) {\n result += fromCodePoint$1(codePoint);\n }\n else if (isValidEscape(codePoint, this.peekCodePoint(0))) {\n result += fromCodePoint$1(this.consumeEscapedCodePoint());\n }\n else {\n this.reconsumeCodePoint(codePoint);\n return result;\n }\n }\n };\n return Tokenizer;\n}());\n\nvar Parser = /** @class */ (function () {\n function Parser(tokens) {\n this._tokens = tokens;\n }\n Parser.create = function (value) {\n var tokenizer = new Tokenizer();\n tokenizer.write(value);\n return new Parser(tokenizer.read());\n };\n Parser.parseValue = function (value) {\n return Parser.create(value).parseComponentValue();\n };\n Parser.parseValues = function (value) {\n return Parser.create(value).parseComponentValues();\n };\n Parser.prototype.parseComponentValue = function () {\n var token = this.consumeToken();\n while (token.type === 31 /* WHITESPACE_TOKEN */) {\n token = this.consumeToken();\n }\n if (token.type === 32 /* EOF_TOKEN */) {\n throw new SyntaxError(\"Error parsing CSS component value, unexpected EOF\");\n }\n this.reconsumeToken(token);\n var value = this.consumeComponentValue();\n do {\n token = this.consumeToken();\n } while (token.type === 31 /* WHITESPACE_TOKEN */);\n if (token.type === 32 /* EOF_TOKEN */) {\n return value;\n }\n throw new SyntaxError(\"Error parsing CSS component value, multiple values found when expecting only one\");\n };\n Parser.prototype.parseComponentValues = function () {\n var values = [];\n while (true) {\n var value = this.consumeComponentValue();\n if (value.type === 32 /* EOF_TOKEN */) {\n return values;\n }\n values.push(value);\n values.push();\n }\n };\n Parser.prototype.consumeComponentValue = function () {\n var token = this.consumeToken();\n switch (token.type) {\n case 11 /* LEFT_CURLY_BRACKET_TOKEN */:\n case 28 /* LEFT_SQUARE_BRACKET_TOKEN */:\n case 2 /* LEFT_PARENTHESIS_TOKEN */:\n return this.consumeSimpleBlock(token.type);\n case 19 /* FUNCTION_TOKEN */:\n return this.consumeFunction(token);\n }\n return token;\n };\n Parser.prototype.consumeSimpleBlock = function (type) {\n var block = { type: type, values: [] };\n var token = this.consumeToken();\n while (true) {\n if (token.type === 32 /* EOF_TOKEN */ || isEndingTokenFor(token, type)) {\n return block;\n }\n this.reconsumeToken(token);\n block.values.push(this.consumeComponentValue());\n token = this.consumeToken();\n }\n };\n Parser.prototype.consumeFunction = function (functionToken) {\n var cssFunction = {\n name: functionToken.value,\n values: [],\n type: 18 /* FUNCTION */\n };\n while (true) {\n var token = this.consumeToken();\n if (token.type === 32 /* EOF_TOKEN */ || token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */) {\n return cssFunction;\n }\n this.reconsumeToken(token);\n cssFunction.values.push(this.consumeComponentValue());\n }\n };\n Parser.prototype.consumeToken = function () {\n var token = this._tokens.shift();\n return typeof token === 'undefined' ? EOF_TOKEN : token;\n };\n Parser.prototype.reconsumeToken = function (token) {\n this._tokens.unshift(token);\n };\n return Parser;\n}());\nvar isDimensionToken = function (token) { return token.type === 15 /* DIMENSION_TOKEN */; };\nvar isNumberToken = function (token) { return token.type === 17 /* NUMBER_TOKEN */; };\nvar isIdentToken = function (token) { return token.type === 20 /* IDENT_TOKEN */; };\nvar isStringToken = function (token) { return token.type === 0 /* STRING_TOKEN */; };\nvar isIdentWithValue = function (token, value) {\n return isIdentToken(token) && token.value === value;\n};\nvar nonWhiteSpace = function (token) { return token.type !== 31 /* WHITESPACE_TOKEN */; };\nvar nonFunctionArgSeparator = function (token) {\n return token.type !== 31 /* WHITESPACE_TOKEN */ && token.type !== 4 /* COMMA_TOKEN */;\n};\nvar parseFunctionArgs = function (tokens) {\n var args = [];\n var arg = [];\n tokens.forEach(function (token) {\n if (token.type === 4 /* COMMA_TOKEN */) {\n if (arg.length === 0) {\n throw new Error(\"Error parsing function args, zero tokens for arg\");\n }\n args.push(arg);\n arg = [];\n return;\n }\n if (token.type !== 31 /* WHITESPACE_TOKEN */) {\n arg.push(token);\n }\n });\n if (arg.length) {\n args.push(arg);\n }\n return args;\n};\nvar isEndingTokenFor = function (token, type) {\n if (type === 11 /* LEFT_CURLY_BRACKET_TOKEN */ && token.type === 12 /* RIGHT_CURLY_BRACKET_TOKEN */) {\n return true;\n }\n if (type === 28 /* LEFT_SQUARE_BRACKET_TOKEN */ && token.type === 29 /* RIGHT_SQUARE_BRACKET_TOKEN */) {\n return true;\n }\n return type === 2 /* LEFT_PARENTHESIS_TOKEN */ && token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */;\n};\n\nvar isLength = function (token) {\n return token.type === 17 /* NUMBER_TOKEN */ || token.type === 15 /* DIMENSION_TOKEN */;\n};\n\nvar isLengthPercentage = function (token) {\n return token.type === 16 /* PERCENTAGE_TOKEN */ || isLength(token);\n};\nvar parseLengthPercentageTuple = function (tokens) {\n return tokens.length > 1 ? [tokens[0], tokens[1]] : [tokens[0]];\n};\nvar ZERO_LENGTH = {\n type: 17 /* NUMBER_TOKEN */,\n number: 0,\n flags: FLAG_INTEGER\n};\nvar FIFTY_PERCENT = {\n type: 16 /* PERCENTAGE_TOKEN */,\n number: 50,\n flags: FLAG_INTEGER\n};\nvar HUNDRED_PERCENT = {\n type: 16 /* PERCENTAGE_TOKEN */,\n number: 100,\n flags: FLAG_INTEGER\n};\nvar getAbsoluteValueForTuple = function (tuple, width, height) {\n var x = tuple[0], y = tuple[1];\n return [getAbsoluteValue(x, width), getAbsoluteValue(typeof y !== 'undefined' ? y : x, height)];\n};\nvar getAbsoluteValue = function (token, parent) {\n if (token.type === 16 /* PERCENTAGE_TOKEN */) {\n return (token.number / 100) * parent;\n }\n if (isDimensionToken(token)) {\n switch (token.unit) {\n case 'rem':\n case 'em':\n return 16 * token.number; // TODO use correct font-size\n case 'px':\n default:\n return token.number;\n }\n }\n return token.number;\n};\n\nvar DEG = 'deg';\nvar GRAD = 'grad';\nvar RAD = 'rad';\nvar TURN = 'turn';\nvar angle = {\n name: 'angle',\n parse: function (_context, value) {\n if (value.type === 15 /* DIMENSION_TOKEN */) {\n switch (value.unit) {\n case DEG:\n return (Math.PI * value.number) / 180;\n case GRAD:\n return (Math.PI / 200) * value.number;\n case RAD:\n return value.number;\n case TURN:\n return Math.PI * 2 * value.number;\n }\n }\n throw new Error(\"Unsupported angle type\");\n }\n};\nvar isAngle = function (value) {\n if (value.type === 15 /* DIMENSION_TOKEN */) {\n if (value.unit === DEG || value.unit === GRAD || value.unit === RAD || value.unit === TURN) {\n return true;\n }\n }\n return false;\n};\nvar parseNamedSide = function (tokens) {\n var sideOrCorner = tokens\n .filter(isIdentToken)\n .map(function (ident) { return ident.value; })\n .join(' ');\n switch (sideOrCorner) {\n case 'to bottom right':\n case 'to right bottom':\n case 'left top':\n case 'top left':\n return [ZERO_LENGTH, ZERO_LENGTH];\n case 'to top':\n case 'bottom':\n return deg(0);\n case 'to bottom left':\n case 'to left bottom':\n case 'right top':\n case 'top right':\n return [ZERO_LENGTH, HUNDRED_PERCENT];\n case 'to right':\n case 'left':\n return deg(90);\n case 'to top left':\n case 'to left top':\n case 'right bottom':\n case 'bottom right':\n return [HUNDRED_PERCENT, HUNDRED_PERCENT];\n case 'to bottom':\n case 'top':\n return deg(180);\n case 'to top right':\n case 'to right top':\n case 'left bottom':\n case 'bottom left':\n return [HUNDRED_PERCENT, ZERO_LENGTH];\n case 'to left':\n case 'right':\n return deg(270);\n }\n return 0;\n};\nvar deg = function (deg) { return (Math.PI * deg) / 180; };\n\nvar color$1 = {\n name: 'color',\n parse: function (context, value) {\n if (value.type === 18 /* FUNCTION */) {\n var colorFunction = SUPPORTED_COLOR_FUNCTIONS[value.name];\n if (typeof colorFunction === 'undefined') {\n throw new Error(\"Attempting to parse an unsupported color function \\\"\" + value.name + \"\\\"\");\n }\n return colorFunction(context, value.values);\n }\n if (value.type === 5 /* HASH_TOKEN */) {\n if (value.value.length === 3) {\n var r = value.value.substring(0, 1);\n var g = value.value.substring(1, 2);\n var b = value.value.substring(2, 3);\n return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), 1);\n }\n if (value.value.length === 4) {\n var r = value.value.substring(0, 1);\n var g = value.value.substring(1, 2);\n var b = value.value.substring(2, 3);\n var a = value.value.substring(3, 4);\n return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255);\n }\n if (value.value.length === 6) {\n var r = value.value.substring(0, 2);\n var g = value.value.substring(2, 4);\n var b = value.value.substring(4, 6);\n return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1);\n }\n if (value.value.length === 8) {\n var r = value.value.substring(0, 2);\n var g = value.value.substring(2, 4);\n var b = value.value.substring(4, 6);\n var a = value.value.substring(6, 8);\n return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), parseInt(a, 16) / 255);\n }\n }\n if (value.type === 20 /* IDENT_TOKEN */) {\n var namedColor = COLORS[value.value.toUpperCase()];\n if (typeof namedColor !== 'undefined') {\n return namedColor;\n }\n }\n return COLORS.TRANSPARENT;\n }\n};\nvar isTransparent = function (color) { return (0xff & color) === 0; };\nvar asString = function (color) {\n var alpha = 0xff & color;\n var blue = 0xff & (color >> 8);\n var green = 0xff & (color >> 16);\n var red = 0xff & (color >> 24);\n return alpha < 255 ? \"rgba(\" + red + \",\" + green + \",\" + blue + \",\" + alpha / 255 + \")\" : \"rgb(\" + red + \",\" + green + \",\" + blue + \")\";\n};\nvar pack = function (r, g, b, a) {\n return ((r << 24) | (g << 16) | (b << 8) | (Math.round(a * 255) << 0)) >>> 0;\n};\nvar getTokenColorValue = function (token, i) {\n if (token.type === 17 /* NUMBER_TOKEN */) {\n return token.number;\n }\n if (token.type === 16 /* PERCENTAGE_TOKEN */) {\n var max = i === 3 ? 1 : 255;\n return i === 3 ? (token.number / 100) * max : Math.round((token.number / 100) * max);\n }\n return 0;\n};\nvar rgb = function (_context, args) {\n var tokens = args.filter(nonFunctionArgSeparator);\n if (tokens.length === 3) {\n var _a = tokens.map(getTokenColorValue), r = _a[0], g = _a[1], b = _a[2];\n return pack(r, g, b, 1);\n }\n if (tokens.length === 4) {\n var _b = tokens.map(getTokenColorValue), r = _b[0], g = _b[1], b = _b[2], a = _b[3];\n return pack(r, g, b, a);\n }\n return 0;\n};\nfunction hue2rgb(t1, t2, hue) {\n if (hue < 0) {\n hue += 1;\n }\n if (hue >= 1) {\n hue -= 1;\n }\n if (hue < 1 / 6) {\n return (t2 - t1) * hue * 6 + t1;\n }\n else if (hue < 1 / 2) {\n return t2;\n }\n else if (hue < 2 / 3) {\n return (t2 - t1) * 6 * (2 / 3 - hue) + t1;\n }\n else {\n return t1;\n }\n}\nvar hsl = function (context, args) {\n var tokens = args.filter(nonFunctionArgSeparator);\n var hue = tokens[0], saturation = tokens[1], lightness = tokens[2], alpha = tokens[3];\n var h = (hue.type === 17 /* NUMBER_TOKEN */ ? deg(hue.number) : angle.parse(context, hue)) / (Math.PI * 2);\n var s = isLengthPercentage(saturation) ? saturation.number / 100 : 0;\n var l = isLengthPercentage(lightness) ? lightness.number / 100 : 0;\n var a = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? getAbsoluteValue(alpha, 1) : 1;\n if (s === 0) {\n return pack(l * 255, l * 255, l * 255, 1);\n }\n var t2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;\n var t1 = l * 2 - t2;\n var r = hue2rgb(t1, t2, h + 1 / 3);\n var g = hue2rgb(t1, t2, h);\n var b = hue2rgb(t1, t2, h - 1 / 3);\n return pack(r * 255, g * 255, b * 255, a);\n};\nvar SUPPORTED_COLOR_FUNCTIONS = {\n hsl: hsl,\n hsla: hsl,\n rgb: rgb,\n rgba: rgb\n};\nvar parseColor = function (context, value) {\n return color$1.parse(context, Parser.create(value).parseComponentValue());\n};\nvar COLORS = {\n ALICEBLUE: 0xf0f8ffff,\n ANTIQUEWHITE: 0xfaebd7ff,\n AQUA: 0x00ffffff,\n AQUAMARINE: 0x7fffd4ff,\n AZURE: 0xf0ffffff,\n BEIGE: 0xf5f5dcff,\n BISQUE: 0xffe4c4ff,\n BLACK: 0x000000ff,\n BLANCHEDALMOND: 0xffebcdff,\n BLUE: 0x0000ffff,\n BLUEVIOLET: 0x8a2be2ff,\n BROWN: 0xa52a2aff,\n BURLYWOOD: 0xdeb887ff,\n CADETBLUE: 0x5f9ea0ff,\n CHARTREUSE: 0x7fff00ff,\n CHOCOLATE: 0xd2691eff,\n CORAL: 0xff7f50ff,\n CORNFLOWERBLUE: 0x6495edff,\n CORNSILK: 0xfff8dcff,\n CRIMSON: 0xdc143cff,\n CYAN: 0x00ffffff,\n DARKBLUE: 0x00008bff,\n DARKCYAN: 0x008b8bff,\n DARKGOLDENROD: 0xb886bbff,\n DARKGRAY: 0xa9a9a9ff,\n DARKGREEN: 0x006400ff,\n DARKGREY: 0xa9a9a9ff,\n DARKKHAKI: 0xbdb76bff,\n DARKMAGENTA: 0x8b008bff,\n DARKOLIVEGREEN: 0x556b2fff,\n DARKORANGE: 0xff8c00ff,\n DARKORCHID: 0x9932ccff,\n DARKRED: 0x8b0000ff,\n DARKSALMON: 0xe9967aff,\n DARKSEAGREEN: 0x8fbc8fff,\n DARKSLATEBLUE: 0x483d8bff,\n DARKSLATEGRAY: 0x2f4f4fff,\n DARKSLATEGREY: 0x2f4f4fff,\n DARKTURQUOISE: 0x00ced1ff,\n DARKVIOLET: 0x9400d3ff,\n DEEPPINK: 0xff1493ff,\n DEEPSKYBLUE: 0x00bfffff,\n DIMGRAY: 0x696969ff,\n DIMGREY: 0x696969ff,\n DODGERBLUE: 0x1e90ffff,\n FIREBRICK: 0xb22222ff,\n FLORALWHITE: 0xfffaf0ff,\n FORESTGREEN: 0x228b22ff,\n FUCHSIA: 0xff00ffff,\n GAINSBORO: 0xdcdcdcff,\n GHOSTWHITE: 0xf8f8ffff,\n GOLD: 0xffd700ff,\n GOLDENROD: 0xdaa520ff,\n GRAY: 0x808080ff,\n GREEN: 0x008000ff,\n GREENYELLOW: 0xadff2fff,\n GREY: 0x808080ff,\n HONEYDEW: 0xf0fff0ff,\n HOTPINK: 0xff69b4ff,\n INDIANRED: 0xcd5c5cff,\n INDIGO: 0x4b0082ff,\n IVORY: 0xfffff0ff,\n KHAKI: 0xf0e68cff,\n LAVENDER: 0xe6e6faff,\n LAVENDERBLUSH: 0xfff0f5ff,\n LAWNGREEN: 0x7cfc00ff,\n LEMONCHIFFON: 0xfffacdff,\n LIGHTBLUE: 0xadd8e6ff,\n LIGHTCORAL: 0xf08080ff,\n LIGHTCYAN: 0xe0ffffff,\n LIGHTGOLDENRODYELLOW: 0xfafad2ff,\n LIGHTGRAY: 0xd3d3d3ff,\n LIGHTGREEN: 0x90ee90ff,\n LIGHTGREY: 0xd3d3d3ff,\n LIGHTPINK: 0xffb6c1ff,\n LIGHTSALMON: 0xffa07aff,\n LIGHTSEAGREEN: 0x20b2aaff,\n LIGHTSKYBLUE: 0x87cefaff,\n LIGHTSLATEGRAY: 0x778899ff,\n LIGHTSLATEGREY: 0x778899ff,\n LIGHTSTEELBLUE: 0xb0c4deff,\n LIGHTYELLOW: 0xffffe0ff,\n LIME: 0x00ff00ff,\n LIMEGREEN: 0x32cd32ff,\n LINEN: 0xfaf0e6ff,\n MAGENTA: 0xff00ffff,\n MAROON: 0x800000ff,\n MEDIUMAQUAMARINE: 0x66cdaaff,\n MEDIUMBLUE: 0x0000cdff,\n MEDIUMORCHID: 0xba55d3ff,\n MEDIUMPURPLE: 0x9370dbff,\n MEDIUMSEAGREEN: 0x3cb371ff,\n MEDIUMSLATEBLUE: 0x7b68eeff,\n MEDIUMSPRINGGREEN: 0x00fa9aff,\n MEDIUMTURQUOISE: 0x48d1ccff,\n MEDIUMVIOLETRED: 0xc71585ff,\n MIDNIGHTBLUE: 0x191970ff,\n MINTCREAM: 0xf5fffaff,\n MISTYROSE: 0xffe4e1ff,\n MOCCASIN: 0xffe4b5ff,\n NAVAJOWHITE: 0xffdeadff,\n NAVY: 0x000080ff,\n OLDLACE: 0xfdf5e6ff,\n OLIVE: 0x808000ff,\n OLIVEDRAB: 0x6b8e23ff,\n ORANGE: 0xffa500ff,\n ORANGERED: 0xff4500ff,\n ORCHID: 0xda70d6ff,\n PALEGOLDENROD: 0xeee8aaff,\n PALEGREEN: 0x98fb98ff,\n PALETURQUOISE: 0xafeeeeff,\n PALEVIOLETRED: 0xdb7093ff,\n PAPAYAWHIP: 0xffefd5ff,\n PEACHPUFF: 0xffdab9ff,\n PERU: 0xcd853fff,\n PINK: 0xffc0cbff,\n PLUM: 0xdda0ddff,\n POWDERBLUE: 0xb0e0e6ff,\n PURPLE: 0x800080ff,\n REBECCAPURPLE: 0x663399ff,\n RED: 0xff0000ff,\n ROSYBROWN: 0xbc8f8fff,\n ROYALBLUE: 0x4169e1ff,\n SADDLEBROWN: 0x8b4513ff,\n SALMON: 0xfa8072ff,\n SANDYBROWN: 0xf4a460ff,\n SEAGREEN: 0x2e8b57ff,\n SEASHELL: 0xfff5eeff,\n SIENNA: 0xa0522dff,\n SILVER: 0xc0c0c0ff,\n SKYBLUE: 0x87ceebff,\n SLATEBLUE: 0x6a5acdff,\n SLATEGRAY: 0x708090ff,\n SLATEGREY: 0x708090ff,\n SNOW: 0xfffafaff,\n SPRINGGREEN: 0x00ff7fff,\n STEELBLUE: 0x4682b4ff,\n TAN: 0xd2b48cff,\n TEAL: 0x008080ff,\n THISTLE: 0xd8bfd8ff,\n TOMATO: 0xff6347ff,\n TRANSPARENT: 0x00000000,\n TURQUOISE: 0x40e0d0ff,\n VIOLET: 0xee82eeff,\n WHEAT: 0xf5deb3ff,\n WHITE: 0xffffffff,\n WHITESMOKE: 0xf5f5f5ff,\n YELLOW: 0xffff00ff,\n YELLOWGREEN: 0x9acd32ff\n};\n\nvar backgroundClip = {\n name: 'background-clip',\n initialValue: 'border-box',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return tokens.map(function (token) {\n if (isIdentToken(token)) {\n switch (token.value) {\n case 'padding-box':\n return 1 /* PADDING_BOX */;\n case 'content-box':\n return 2 /* CONTENT_BOX */;\n }\n }\n return 0 /* BORDER_BOX */;\n });\n }\n};\n\nvar backgroundColor = {\n name: \"background-color\",\n initialValue: 'transparent',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'color'\n};\n\nvar parseColorStop = function (context, args) {\n var color = color$1.parse(context, args[0]);\n var stop = args[1];\n return stop && isLengthPercentage(stop) ? { color: color, stop: stop } : { color: color, stop: null };\n};\nvar processColorStops = function (stops, lineLength) {\n var first = stops[0];\n var last = stops[stops.length - 1];\n if (first.stop === null) {\n first.stop = ZERO_LENGTH;\n }\n if (last.stop === null) {\n last.stop = HUNDRED_PERCENT;\n }\n var processStops = [];\n var previous = 0;\n for (var i = 0; i < stops.length; i++) {\n var stop_1 = stops[i].stop;\n if (stop_1 !== null) {\n var absoluteValue = getAbsoluteValue(stop_1, lineLength);\n if (absoluteValue > previous) {\n processStops.push(absoluteValue);\n }\n else {\n processStops.push(previous);\n }\n previous = absoluteValue;\n }\n else {\n processStops.push(null);\n }\n }\n var gapBegin = null;\n for (var i = 0; i < processStops.length; i++) {\n var stop_2 = processStops[i];\n if (stop_2 === null) {\n if (gapBegin === null) {\n gapBegin = i;\n }\n }\n else if (gapBegin !== null) {\n var gapLength = i - gapBegin;\n var beforeGap = processStops[gapBegin - 1];\n var gapValue = (stop_2 - beforeGap) / (gapLength + 1);\n for (var g = 1; g <= gapLength; g++) {\n processStops[gapBegin + g - 1] = gapValue * g;\n }\n gapBegin = null;\n }\n }\n return stops.map(function (_a, i) {\n var color = _a.color;\n return { color: color, stop: Math.max(Math.min(1, processStops[i] / lineLength), 0) };\n });\n};\nvar getAngleFromCorner = function (corner, width, height) {\n var centerX = width / 2;\n var centerY = height / 2;\n var x = getAbsoluteValue(corner[0], width) - centerX;\n var y = centerY - getAbsoluteValue(corner[1], height);\n return (Math.atan2(y, x) + Math.PI * 2) % (Math.PI * 2);\n};\nvar calculateGradientDirection = function (angle, width, height) {\n var radian = typeof angle === 'number' ? angle : getAngleFromCorner(angle, width, height);\n var lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));\n var halfWidth = width / 2;\n var halfHeight = height / 2;\n var halfLineLength = lineLength / 2;\n var yDiff = Math.sin(radian - Math.PI / 2) * halfLineLength;\n var xDiff = Math.cos(radian - Math.PI / 2) * halfLineLength;\n return [lineLength, halfWidth - xDiff, halfWidth + xDiff, halfHeight - yDiff, halfHeight + yDiff];\n};\nvar distance = function (a, b) { return Math.sqrt(a * a + b * b); };\nvar findCorner = function (width, height, x, y, closest) {\n var corners = [\n [0, 0],\n [0, height],\n [width, 0],\n [width, height]\n ];\n return corners.reduce(function (stat, corner) {\n var cx = corner[0], cy = corner[1];\n var d = distance(x - cx, y - cy);\n if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) {\n return {\n optimumCorner: corner,\n optimumDistance: d\n };\n }\n return stat;\n }, {\n optimumDistance: closest ? Infinity : -Infinity,\n optimumCorner: null\n }).optimumCorner;\n};\nvar calculateRadius = function (gradient, x, y, width, height) {\n var rx = 0;\n var ry = 0;\n switch (gradient.size) {\n case 0 /* CLOSEST_SIDE */:\n // The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradient’s center.\n // If the shape is an ellipse, it exactly meets the closest side in each dimension.\n if (gradient.shape === 0 /* CIRCLE */) {\n rx = ry = Math.min(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height));\n }\n else if (gradient.shape === 1 /* ELLIPSE */) {\n rx = Math.min(Math.abs(x), Math.abs(x - width));\n ry = Math.min(Math.abs(y), Math.abs(y - height));\n }\n break;\n case 2 /* CLOSEST_CORNER */:\n // The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradient’s center.\n // If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.\n if (gradient.shape === 0 /* CIRCLE */) {\n rx = ry = Math.min(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height));\n }\n else if (gradient.shape === 1 /* ELLIPSE */) {\n // Compute the ratio ry/rx (which is to be the same as for \"closest-side\")\n var c = Math.min(Math.abs(y), Math.abs(y - height)) / Math.min(Math.abs(x), Math.abs(x - width));\n var _a = findCorner(width, height, x, y, true), cx = _a[0], cy = _a[1];\n rx = distance(cx - x, (cy - y) / c);\n ry = c * rx;\n }\n break;\n case 1 /* FARTHEST_SIDE */:\n // Same as closest-side, except the ending shape is sized based on the farthest side(s)\n if (gradient.shape === 0 /* CIRCLE */) {\n rx = ry = Math.max(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height));\n }\n else if (gradient.shape === 1 /* ELLIPSE */) {\n rx = Math.max(Math.abs(x), Math.abs(x - width));\n ry = Math.max(Math.abs(y), Math.abs(y - height));\n }\n break;\n case 3 /* FARTHEST_CORNER */:\n // Same as closest-corner, except the ending shape is sized based on the farthest corner.\n // If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.\n if (gradient.shape === 0 /* CIRCLE */) {\n rx = ry = Math.max(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height));\n }\n else if (gradient.shape === 1 /* ELLIPSE */) {\n // Compute the ratio ry/rx (which is to be the same as for \"farthest-side\")\n var c = Math.max(Math.abs(y), Math.abs(y - height)) / Math.max(Math.abs(x), Math.abs(x - width));\n var _b = findCorner(width, height, x, y, false), cx = _b[0], cy = _b[1];\n rx = distance(cx - x, (cy - y) / c);\n ry = c * rx;\n }\n break;\n }\n if (Array.isArray(gradient.size)) {\n rx = getAbsoluteValue(gradient.size[0], width);\n ry = gradient.size.length === 2 ? getAbsoluteValue(gradient.size[1], height) : rx;\n }\n return [rx, ry];\n};\n\nvar linearGradient = function (context, tokens) {\n var angle$1 = deg(180);\n var stops = [];\n parseFunctionArgs(tokens).forEach(function (arg, i) {\n if (i === 0) {\n var firstToken = arg[0];\n if (firstToken.type === 20 /* IDENT_TOKEN */ && firstToken.value === 'to') {\n angle$1 = parseNamedSide(arg);\n return;\n }\n else if (isAngle(firstToken)) {\n angle$1 = angle.parse(context, firstToken);\n return;\n }\n }\n var colorStop = parseColorStop(context, arg);\n stops.push(colorStop);\n });\n return { angle: angle$1, stops: stops, type: 1 /* LINEAR_GRADIENT */ };\n};\n\nvar prefixLinearGradient = function (context, tokens) {\n var angle$1 = deg(180);\n var stops = [];\n parseFunctionArgs(tokens).forEach(function (arg, i) {\n if (i === 0) {\n var firstToken = arg[0];\n if (firstToken.type === 20 /* IDENT_TOKEN */ &&\n ['top', 'left', 'right', 'bottom'].indexOf(firstToken.value) !== -1) {\n angle$1 = parseNamedSide(arg);\n return;\n }\n else if (isAngle(firstToken)) {\n angle$1 = (angle.parse(context, firstToken) + deg(270)) % deg(360);\n return;\n }\n }\n var colorStop = parseColorStop(context, arg);\n stops.push(colorStop);\n });\n return {\n angle: angle$1,\n stops: stops,\n type: 1 /* LINEAR_GRADIENT */\n };\n};\n\nvar webkitGradient = function (context, tokens) {\n var angle = deg(180);\n var stops = [];\n var type = 1 /* LINEAR_GRADIENT */;\n var shape = 0 /* CIRCLE */;\n var size = 3 /* FARTHEST_CORNER */;\n var position = [];\n parseFunctionArgs(tokens).forEach(function (arg, i) {\n var firstToken = arg[0];\n if (i === 0) {\n if (isIdentToken(firstToken) && firstToken.value === 'linear') {\n type = 1 /* LINEAR_GRADIENT */;\n return;\n }\n else if (isIdentToken(firstToken) && firstToken.value === 'radial') {\n type = 2 /* RADIAL_GRADIENT */;\n return;\n }\n }\n if (firstToken.type === 18 /* FUNCTION */) {\n if (firstToken.name === 'from') {\n var color = color$1.parse(context, firstToken.values[0]);\n stops.push({ stop: ZERO_LENGTH, color: color });\n }\n else if (firstToken.name === 'to') {\n var color = color$1.parse(context, firstToken.values[0]);\n stops.push({ stop: HUNDRED_PERCENT, color: color });\n }\n else if (firstToken.name === 'color-stop') {\n var values = firstToken.values.filter(nonFunctionArgSeparator);\n if (values.length === 2) {\n var color = color$1.parse(context, values[1]);\n var stop_1 = values[0];\n if (isNumberToken(stop_1)) {\n stops.push({\n stop: { type: 16 /* PERCENTAGE_TOKEN */, number: stop_1.number * 100, flags: stop_1.flags },\n color: color\n });\n }\n }\n }\n }\n });\n return type === 1 /* LINEAR_GRADIENT */\n ? {\n angle: (angle + deg(180)) % deg(360),\n stops: stops,\n type: type\n }\n : { size: size, shape: shape, stops: stops, position: position, type: type };\n};\n\nvar CLOSEST_SIDE = 'closest-side';\nvar FARTHEST_SIDE = 'farthest-side';\nvar CLOSEST_CORNER = 'closest-corner';\nvar FARTHEST_CORNER = 'farthest-corner';\nvar CIRCLE = 'circle';\nvar ELLIPSE = 'ellipse';\nvar COVER = 'cover';\nvar CONTAIN = 'contain';\nvar radialGradient = function (context, tokens) {\n var shape = 0 /* CIRCLE */;\n var size = 3 /* FARTHEST_CORNER */;\n var stops = [];\n var position = [];\n parseFunctionArgs(tokens).forEach(function (arg, i) {\n var isColorStop = true;\n if (i === 0) {\n var isAtPosition_1 = false;\n isColorStop = arg.reduce(function (acc, token) {\n if (isAtPosition_1) {\n if (isIdentToken(token)) {\n switch (token.value) {\n case 'center':\n position.push(FIFTY_PERCENT);\n return acc;\n case 'top':\n case 'left':\n position.push(ZERO_LENGTH);\n return acc;\n case 'right':\n case 'bottom':\n position.push(HUNDRED_PERCENT);\n return acc;\n }\n }\n else if (isLengthPercentage(token) || isLength(token)) {\n position.push(token);\n }\n }\n else if (isIdentToken(token)) {\n switch (token.value) {\n case CIRCLE:\n shape = 0 /* CIRCLE */;\n return false;\n case ELLIPSE:\n shape = 1 /* ELLIPSE */;\n return false;\n case 'at':\n isAtPosition_1 = true;\n return false;\n case CLOSEST_SIDE:\n size = 0 /* CLOSEST_SIDE */;\n return false;\n case COVER:\n case FARTHEST_SIDE:\n size = 1 /* FARTHEST_SIDE */;\n return false;\n case CONTAIN:\n case CLOSEST_CORNER:\n size = 2 /* CLOSEST_CORNER */;\n return false;\n case FARTHEST_CORNER:\n size = 3 /* FARTHEST_CORNER */;\n return false;\n }\n }\n else if (isLength(token) || isLengthPercentage(token)) {\n if (!Array.isArray(size)) {\n size = [];\n }\n size.push(token);\n return false;\n }\n return acc;\n }, isColorStop);\n }\n if (isColorStop) {\n var colorStop = parseColorStop(context, arg);\n stops.push(colorStop);\n }\n });\n return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ };\n};\n\nvar prefixRadialGradient = function (context, tokens) {\n var shape = 0 /* CIRCLE */;\n var size = 3 /* FARTHEST_CORNER */;\n var stops = [];\n var position = [];\n parseFunctionArgs(tokens).forEach(function (arg, i) {\n var isColorStop = true;\n if (i === 0) {\n isColorStop = arg.reduce(function (acc, token) {\n if (isIdentToken(token)) {\n switch (token.value) {\n case 'center':\n position.push(FIFTY_PERCENT);\n return false;\n case 'top':\n case 'left':\n position.push(ZERO_LENGTH);\n return false;\n case 'right':\n case 'bottom':\n position.push(HUNDRED_PERCENT);\n return false;\n }\n }\n else if (isLengthPercentage(token) || isLength(token)) {\n position.push(token);\n return false;\n }\n return acc;\n }, isColorStop);\n }\n else if (i === 1) {\n isColorStop = arg.reduce(function (acc, token) {\n if (isIdentToken(token)) {\n switch (token.value) {\n case CIRCLE:\n shape = 0 /* CIRCLE */;\n return false;\n case ELLIPSE:\n shape = 1 /* ELLIPSE */;\n return false;\n case CONTAIN:\n case CLOSEST_SIDE:\n size = 0 /* CLOSEST_SIDE */;\n return false;\n case FARTHEST_SIDE:\n size = 1 /* FARTHEST_SIDE */;\n return false;\n case CLOSEST_CORNER:\n size = 2 /* CLOSEST_CORNER */;\n return false;\n case COVER:\n case FARTHEST_CORNER:\n size = 3 /* FARTHEST_CORNER */;\n return false;\n }\n }\n else if (isLength(token) || isLengthPercentage(token)) {\n if (!Array.isArray(size)) {\n size = [];\n }\n size.push(token);\n return false;\n }\n return acc;\n }, isColorStop);\n }\n if (isColorStop) {\n var colorStop = parseColorStop(context, arg);\n stops.push(colorStop);\n }\n });\n return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ };\n};\n\nvar isLinearGradient = function (background) {\n return background.type === 1 /* LINEAR_GRADIENT */;\n};\nvar isRadialGradient = function (background) {\n return background.type === 2 /* RADIAL_GRADIENT */;\n};\nvar image = {\n name: 'image',\n parse: function (context, value) {\n if (value.type === 22 /* URL_TOKEN */) {\n var image_1 = { url: value.value, type: 0 /* URL */ };\n context.cache.addImage(value.value);\n return image_1;\n }\n if (value.type === 18 /* FUNCTION */) {\n var imageFunction = SUPPORTED_IMAGE_FUNCTIONS[value.name];\n if (typeof imageFunction === 'undefined') {\n throw new Error(\"Attempting to parse an unsupported image function \\\"\" + value.name + \"\\\"\");\n }\n return imageFunction(context, value.values);\n }\n throw new Error(\"Unsupported image type \" + value.type);\n }\n};\nfunction isSupportedImage(value) {\n return (!(value.type === 20 /* IDENT_TOKEN */ && value.value === 'none') &&\n (value.type !== 18 /* FUNCTION */ || !!SUPPORTED_IMAGE_FUNCTIONS[value.name]));\n}\nvar SUPPORTED_IMAGE_FUNCTIONS = {\n 'linear-gradient': linearGradient,\n '-moz-linear-gradient': prefixLinearGradient,\n '-ms-linear-gradient': prefixLinearGradient,\n '-o-linear-gradient': prefixLinearGradient,\n '-webkit-linear-gradient': prefixLinearGradient,\n 'radial-gradient': radialGradient,\n '-moz-radial-gradient': prefixRadialGradient,\n '-ms-radial-gradient': prefixRadialGradient,\n '-o-radial-gradient': prefixRadialGradient,\n '-webkit-radial-gradient': prefixRadialGradient,\n '-webkit-gradient': webkitGradient\n};\n\nvar backgroundImage = {\n name: 'background-image',\n initialValue: 'none',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (context, tokens) {\n if (tokens.length === 0) {\n return [];\n }\n var first = tokens[0];\n if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {\n return [];\n }\n return tokens\n .filter(function (value) { return nonFunctionArgSeparator(value) && isSupportedImage(value); })\n .map(function (value) { return image.parse(context, value); });\n }\n};\n\nvar backgroundOrigin = {\n name: 'background-origin',\n initialValue: 'border-box',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return tokens.map(function (token) {\n if (isIdentToken(token)) {\n switch (token.value) {\n case 'padding-box':\n return 1 /* PADDING_BOX */;\n case 'content-box':\n return 2 /* CONTENT_BOX */;\n }\n }\n return 0 /* BORDER_BOX */;\n });\n }\n};\n\nvar backgroundPosition = {\n name: 'background-position',\n initialValue: '0% 0%',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (_context, tokens) {\n return parseFunctionArgs(tokens)\n .map(function (values) { return values.filter(isLengthPercentage); })\n .map(parseLengthPercentageTuple);\n }\n};\n\nvar backgroundRepeat = {\n name: 'background-repeat',\n initialValue: 'repeat',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return parseFunctionArgs(tokens)\n .map(function (values) {\n return values\n .filter(isIdentToken)\n .map(function (token) { return token.value; })\n .join(' ');\n })\n .map(parseBackgroundRepeat);\n }\n};\nvar parseBackgroundRepeat = function (value) {\n switch (value) {\n case 'no-repeat':\n return 1 /* NO_REPEAT */;\n case 'repeat-x':\n case 'repeat no-repeat':\n return 2 /* REPEAT_X */;\n case 'repeat-y':\n case 'no-repeat repeat':\n return 3 /* REPEAT_Y */;\n case 'repeat':\n default:\n return 0 /* REPEAT */;\n }\n};\n\nvar BACKGROUND_SIZE;\n(function (BACKGROUND_SIZE) {\n BACKGROUND_SIZE[\"AUTO\"] = \"auto\";\n BACKGROUND_SIZE[\"CONTAIN\"] = \"contain\";\n BACKGROUND_SIZE[\"COVER\"] = \"cover\";\n})(BACKGROUND_SIZE || (BACKGROUND_SIZE = {}));\nvar backgroundSize = {\n name: 'background-size',\n initialValue: '0',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return parseFunctionArgs(tokens).map(function (values) { return values.filter(isBackgroundSizeInfoToken); });\n }\n};\nvar isBackgroundSizeInfoToken = function (value) {\n return isIdentToken(value) || isLengthPercentage(value);\n};\n\nvar borderColorForSide = function (side) { return ({\n name: \"border-\" + side + \"-color\",\n initialValue: 'transparent',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'color'\n}); };\nvar borderTopColor = borderColorForSide('top');\nvar borderRightColor = borderColorForSide('right');\nvar borderBottomColor = borderColorForSide('bottom');\nvar borderLeftColor = borderColorForSide('left');\n\nvar borderRadiusForSide = function (side) { return ({\n name: \"border-radius-\" + side,\n initialValue: '0 0',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return parseLengthPercentageTuple(tokens.filter(isLengthPercentage));\n }\n}); };\nvar borderTopLeftRadius = borderRadiusForSide('top-left');\nvar borderTopRightRadius = borderRadiusForSide('top-right');\nvar borderBottomRightRadius = borderRadiusForSide('bottom-right');\nvar borderBottomLeftRadius = borderRadiusForSide('bottom-left');\n\nvar borderStyleForSide = function (side) { return ({\n name: \"border-\" + side + \"-style\",\n initialValue: 'solid',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, style) {\n switch (style) {\n case 'none':\n return 0 /* NONE */;\n case 'dashed':\n return 2 /* DASHED */;\n case 'dotted':\n return 3 /* DOTTED */;\n case 'double':\n return 4 /* DOUBLE */;\n }\n return 1 /* SOLID */;\n }\n}); };\nvar borderTopStyle = borderStyleForSide('top');\nvar borderRightStyle = borderStyleForSide('right');\nvar borderBottomStyle = borderStyleForSide('bottom');\nvar borderLeftStyle = borderStyleForSide('left');\n\nvar borderWidthForSide = function (side) { return ({\n name: \"border-\" + side + \"-width\",\n initialValue: '0',\n type: 0 /* VALUE */,\n prefix: false,\n parse: function (_context, token) {\n if (isDimensionToken(token)) {\n return token.number;\n }\n return 0;\n }\n}); };\nvar borderTopWidth = borderWidthForSide('top');\nvar borderRightWidth = borderWidthForSide('right');\nvar borderBottomWidth = borderWidthForSide('bottom');\nvar borderLeftWidth = borderWidthForSide('left');\n\nvar color = {\n name: \"color\",\n initialValue: 'transparent',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'color'\n};\n\nvar direction = {\n name: 'direction',\n initialValue: 'ltr',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, direction) {\n switch (direction) {\n case 'rtl':\n return 1 /* RTL */;\n case 'ltr':\n default:\n return 0 /* LTR */;\n }\n }\n};\n\nvar display = {\n name: 'display',\n initialValue: 'inline-block',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return tokens.filter(isIdentToken).reduce(function (bit, token) {\n return bit | parseDisplayValue(token.value);\n }, 0 /* NONE */);\n }\n};\nvar parseDisplayValue = function (display) {\n switch (display) {\n case 'block':\n case '-webkit-box':\n return 2 /* BLOCK */;\n case 'inline':\n return 4 /* INLINE */;\n case 'run-in':\n return 8 /* RUN_IN */;\n case 'flow':\n return 16 /* FLOW */;\n case 'flow-root':\n return 32 /* FLOW_ROOT */;\n case 'table':\n return 64 /* TABLE */;\n case 'flex':\n case '-webkit-flex':\n return 128 /* FLEX */;\n case 'grid':\n case '-ms-grid':\n return 256 /* GRID */;\n case 'ruby':\n return 512 /* RUBY */;\n case 'subgrid':\n return 1024 /* SUBGRID */;\n case 'list-item':\n return 2048 /* LIST_ITEM */;\n case 'table-row-group':\n return 4096 /* TABLE_ROW_GROUP */;\n case 'table-header-group':\n return 8192 /* TABLE_HEADER_GROUP */;\n case 'table-footer-group':\n return 16384 /* TABLE_FOOTER_GROUP */;\n case 'table-row':\n return 32768 /* TABLE_ROW */;\n case 'table-cell':\n return 65536 /* TABLE_CELL */;\n case 'table-column-group':\n return 131072 /* TABLE_COLUMN_GROUP */;\n case 'table-column':\n return 262144 /* TABLE_COLUMN */;\n case 'table-caption':\n return 524288 /* TABLE_CAPTION */;\n case 'ruby-base':\n return 1048576 /* RUBY_BASE */;\n case 'ruby-text':\n return 2097152 /* RUBY_TEXT */;\n case 'ruby-base-container':\n return 4194304 /* RUBY_BASE_CONTAINER */;\n case 'ruby-text-container':\n return 8388608 /* RUBY_TEXT_CONTAINER */;\n case 'contents':\n return 16777216 /* CONTENTS */;\n case 'inline-block':\n return 33554432 /* INLINE_BLOCK */;\n case 'inline-list-item':\n return 67108864 /* INLINE_LIST_ITEM */;\n case 'inline-table':\n return 134217728 /* INLINE_TABLE */;\n case 'inline-flex':\n return 268435456 /* INLINE_FLEX */;\n case 'inline-grid':\n return 536870912 /* INLINE_GRID */;\n }\n return 0 /* NONE */;\n};\n\nvar float = {\n name: 'float',\n initialValue: 'none',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, float) {\n switch (float) {\n case 'left':\n return 1 /* LEFT */;\n case 'right':\n return 2 /* RIGHT */;\n case 'inline-start':\n return 3 /* INLINE_START */;\n case 'inline-end':\n return 4 /* INLINE_END */;\n }\n return 0 /* NONE */;\n }\n};\n\nvar letterSpacing = {\n name: 'letter-spacing',\n initialValue: '0',\n prefix: false,\n type: 0 /* VALUE */,\n parse: function (_context, token) {\n if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'normal') {\n return 0;\n }\n if (token.type === 17 /* NUMBER_TOKEN */) {\n return token.number;\n }\n if (token.type === 15 /* DIMENSION_TOKEN */) {\n return token.number;\n }\n return 0;\n }\n};\n\nvar LINE_BREAK;\n(function (LINE_BREAK) {\n LINE_BREAK[\"NORMAL\"] = \"normal\";\n LINE_BREAK[\"STRICT\"] = \"strict\";\n})(LINE_BREAK || (LINE_BREAK = {}));\nvar lineBreak = {\n name: 'line-break',\n initialValue: 'normal',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, lineBreak) {\n switch (lineBreak) {\n case 'strict':\n return LINE_BREAK.STRICT;\n case 'normal':\n default:\n return LINE_BREAK.NORMAL;\n }\n }\n};\n\nvar lineHeight = {\n name: 'line-height',\n initialValue: 'normal',\n prefix: false,\n type: 4 /* TOKEN_VALUE */\n};\nvar computeLineHeight = function (token, fontSize) {\n if (isIdentToken(token) && token.value === 'normal') {\n return 1.2 * fontSize;\n }\n else if (token.type === 17 /* NUMBER_TOKEN */) {\n return fontSize * token.number;\n }\n else if (isLengthPercentage(token)) {\n return getAbsoluteValue(token, fontSize);\n }\n return fontSize;\n};\n\nvar listStyleImage = {\n name: 'list-style-image',\n initialValue: 'none',\n type: 0 /* VALUE */,\n prefix: false,\n parse: function (context, token) {\n if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') {\n return null;\n }\n return image.parse(context, token);\n }\n};\n\nvar listStylePosition = {\n name: 'list-style-position',\n initialValue: 'outside',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, position) {\n switch (position) {\n case 'inside':\n return 0 /* INSIDE */;\n case 'outside':\n default:\n return 1 /* OUTSIDE */;\n }\n }\n};\n\nvar listStyleType = {\n name: 'list-style-type',\n initialValue: 'none',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, type) {\n switch (type) {\n case 'disc':\n return 0 /* DISC */;\n case 'circle':\n return 1 /* CIRCLE */;\n case 'square':\n return 2 /* SQUARE */;\n case 'decimal':\n return 3 /* DECIMAL */;\n case 'cjk-decimal':\n return 4 /* CJK_DECIMAL */;\n case 'decimal-leading-zero':\n return 5 /* DECIMAL_LEADING_ZERO */;\n case 'lower-roman':\n return 6 /* LOWER_ROMAN */;\n case 'upper-roman':\n return 7 /* UPPER_ROMAN */;\n case 'lower-greek':\n return 8 /* LOWER_GREEK */;\n case 'lower-alpha':\n return 9 /* LOWER_ALPHA */;\n case 'upper-alpha':\n return 10 /* UPPER_ALPHA */;\n case 'arabic-indic':\n return 11 /* ARABIC_INDIC */;\n case 'armenian':\n return 12 /* ARMENIAN */;\n case 'bengali':\n return 13 /* BENGALI */;\n case 'cambodian':\n return 14 /* CAMBODIAN */;\n case 'cjk-earthly-branch':\n return 15 /* CJK_EARTHLY_BRANCH */;\n case 'cjk-heavenly-stem':\n return 16 /* CJK_HEAVENLY_STEM */;\n case 'cjk-ideographic':\n return 17 /* CJK_IDEOGRAPHIC */;\n case 'devanagari':\n return 18 /* DEVANAGARI */;\n case 'ethiopic-numeric':\n return 19 /* ETHIOPIC_NUMERIC */;\n case 'georgian':\n return 20 /* GEORGIAN */;\n case 'gujarati':\n return 21 /* GUJARATI */;\n case 'gurmukhi':\n return 22 /* GURMUKHI */;\n case 'hebrew':\n return 22 /* HEBREW */;\n case 'hiragana':\n return 23 /* HIRAGANA */;\n case 'hiragana-iroha':\n return 24 /* HIRAGANA_IROHA */;\n case 'japanese-formal':\n return 25 /* JAPANESE_FORMAL */;\n case 'japanese-informal':\n return 26 /* JAPANESE_INFORMAL */;\n case 'kannada':\n return 27 /* KANNADA */;\n case 'katakana':\n return 28 /* KATAKANA */;\n case 'katakana-iroha':\n return 29 /* KATAKANA_IROHA */;\n case 'khmer':\n return 30 /* KHMER */;\n case 'korean-hangul-formal':\n return 31 /* KOREAN_HANGUL_FORMAL */;\n case 'korean-hanja-formal':\n return 32 /* KOREAN_HANJA_FORMAL */;\n case 'korean-hanja-informal':\n return 33 /* KOREAN_HANJA_INFORMAL */;\n case 'lao':\n return 34 /* LAO */;\n case 'lower-armenian':\n return 35 /* LOWER_ARMENIAN */;\n case 'malayalam':\n return 36 /* MALAYALAM */;\n case 'mongolian':\n return 37 /* MONGOLIAN */;\n case 'myanmar':\n return 38 /* MYANMAR */;\n case 'oriya':\n return 39 /* ORIYA */;\n case 'persian':\n return 40 /* PERSIAN */;\n case 'simp-chinese-formal':\n return 41 /* SIMP_CHINESE_FORMAL */;\n case 'simp-chinese-informal':\n return 42 /* SIMP_CHINESE_INFORMAL */;\n case 'tamil':\n return 43 /* TAMIL */;\n case 'telugu':\n return 44 /* TELUGU */;\n case 'thai':\n return 45 /* THAI */;\n case 'tibetan':\n return 46 /* TIBETAN */;\n case 'trad-chinese-formal':\n return 47 /* TRAD_CHINESE_FORMAL */;\n case 'trad-chinese-informal':\n return 48 /* TRAD_CHINESE_INFORMAL */;\n case 'upper-armenian':\n return 49 /* UPPER_ARMENIAN */;\n case 'disclosure-open':\n return 50 /* DISCLOSURE_OPEN */;\n case 'disclosure-closed':\n return 51 /* DISCLOSURE_CLOSED */;\n case 'none':\n default:\n return -1 /* NONE */;\n }\n }\n};\n\nvar marginForSide = function (side) { return ({\n name: \"margin-\" + side,\n initialValue: '0',\n prefix: false,\n type: 4 /* TOKEN_VALUE */\n}); };\nvar marginTop = marginForSide('top');\nvar marginRight = marginForSide('right');\nvar marginBottom = marginForSide('bottom');\nvar marginLeft = marginForSide('left');\n\nvar overflow = {\n name: 'overflow',\n initialValue: 'visible',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return tokens.filter(isIdentToken).map(function (overflow) {\n switch (overflow.value) {\n case 'hidden':\n return 1 /* HIDDEN */;\n case 'scroll':\n return 2 /* SCROLL */;\n case 'clip':\n return 3 /* CLIP */;\n case 'auto':\n return 4 /* AUTO */;\n case 'visible':\n default:\n return 0 /* VISIBLE */;\n }\n });\n }\n};\n\nvar overflowWrap = {\n name: 'overflow-wrap',\n initialValue: 'normal',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, overflow) {\n switch (overflow) {\n case 'break-word':\n return \"break-word\" /* BREAK_WORD */;\n case 'normal':\n default:\n return \"normal\" /* NORMAL */;\n }\n }\n};\n\nvar paddingForSide = function (side) { return ({\n name: \"padding-\" + side,\n initialValue: '0',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'length-percentage'\n}); };\nvar paddingTop = paddingForSide('top');\nvar paddingRight = paddingForSide('right');\nvar paddingBottom = paddingForSide('bottom');\nvar paddingLeft = paddingForSide('left');\n\nvar textAlign = {\n name: 'text-align',\n initialValue: 'left',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, textAlign) {\n switch (textAlign) {\n case 'right':\n return 2 /* RIGHT */;\n case 'center':\n case 'justify':\n return 1 /* CENTER */;\n case 'left':\n default:\n return 0 /* LEFT */;\n }\n }\n};\n\nvar position = {\n name: 'position',\n initialValue: 'static',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, position) {\n switch (position) {\n case 'relative':\n return 1 /* RELATIVE */;\n case 'absolute':\n return 2 /* ABSOLUTE */;\n case 'fixed':\n return 3 /* FIXED */;\n case 'sticky':\n return 4 /* STICKY */;\n }\n return 0 /* STATIC */;\n }\n};\n\nvar textShadow = {\n name: 'text-shadow',\n initialValue: 'none',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (context, tokens) {\n if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {\n return [];\n }\n return parseFunctionArgs(tokens).map(function (values) {\n var shadow = {\n color: COLORS.TRANSPARENT,\n offsetX: ZERO_LENGTH,\n offsetY: ZERO_LENGTH,\n blur: ZERO_LENGTH\n };\n var c = 0;\n for (var i = 0; i < values.length; i++) {\n var token = values[i];\n if (isLength(token)) {\n if (c === 0) {\n shadow.offsetX = token;\n }\n else if (c === 1) {\n shadow.offsetY = token;\n }\n else {\n shadow.blur = token;\n }\n c++;\n }\n else {\n shadow.color = color$1.parse(context, token);\n }\n }\n return shadow;\n });\n }\n};\n\nvar textTransform = {\n name: 'text-transform',\n initialValue: 'none',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, textTransform) {\n switch (textTransform) {\n case 'uppercase':\n return 2 /* UPPERCASE */;\n case 'lowercase':\n return 1 /* LOWERCASE */;\n case 'capitalize':\n return 3 /* CAPITALIZE */;\n }\n return 0 /* NONE */;\n }\n};\n\nvar transform$1 = {\n name: 'transform',\n initialValue: 'none',\n prefix: true,\n type: 0 /* VALUE */,\n parse: function (_context, token) {\n if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') {\n return null;\n }\n if (token.type === 18 /* FUNCTION */) {\n var transformFunction = SUPPORTED_TRANSFORM_FUNCTIONS[token.name];\n if (typeof transformFunction === 'undefined') {\n throw new Error(\"Attempting to parse an unsupported transform function \\\"\" + token.name + \"\\\"\");\n }\n return transformFunction(token.values);\n }\n return null;\n }\n};\nvar matrix = function (args) {\n var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; });\n return values.length === 6 ? values : null;\n};\n// doesn't support 3D transforms at the moment\nvar matrix3d = function (args) {\n var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; });\n var a1 = values[0], b1 = values[1]; values[2]; values[3]; var a2 = values[4], b2 = values[5]; values[6]; values[7]; values[8]; values[9]; values[10]; values[11]; var a4 = values[12], b4 = values[13]; values[14]; values[15];\n return values.length === 16 ? [a1, b1, a2, b2, a4, b4] : null;\n};\nvar SUPPORTED_TRANSFORM_FUNCTIONS = {\n matrix: matrix,\n matrix3d: matrix3d\n};\n\nvar DEFAULT_VALUE = {\n type: 16 /* PERCENTAGE_TOKEN */,\n number: 50,\n flags: FLAG_INTEGER\n};\nvar DEFAULT = [DEFAULT_VALUE, DEFAULT_VALUE];\nvar transformOrigin = {\n name: 'transform-origin',\n initialValue: '50% 50%',\n prefix: true,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n var origins = tokens.filter(isLengthPercentage);\n if (origins.length !== 2) {\n return DEFAULT;\n }\n return [origins[0], origins[1]];\n }\n};\n\nvar visibility = {\n name: 'visible',\n initialValue: 'none',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, visibility) {\n switch (visibility) {\n case 'hidden':\n return 1 /* HIDDEN */;\n case 'collapse':\n return 2 /* COLLAPSE */;\n case 'visible':\n default:\n return 0 /* VISIBLE */;\n }\n }\n};\n\nvar WORD_BREAK;\n(function (WORD_BREAK) {\n WORD_BREAK[\"NORMAL\"] = \"normal\";\n WORD_BREAK[\"BREAK_ALL\"] = \"break-all\";\n WORD_BREAK[\"KEEP_ALL\"] = \"keep-all\";\n})(WORD_BREAK || (WORD_BREAK = {}));\nvar wordBreak = {\n name: 'word-break',\n initialValue: 'normal',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, wordBreak) {\n switch (wordBreak) {\n case 'break-all':\n return WORD_BREAK.BREAK_ALL;\n case 'keep-all':\n return WORD_BREAK.KEEP_ALL;\n case 'normal':\n default:\n return WORD_BREAK.NORMAL;\n }\n }\n};\n\nvar zIndex = {\n name: 'z-index',\n initialValue: 'auto',\n prefix: false,\n type: 0 /* VALUE */,\n parse: function (_context, token) {\n if (token.type === 20 /* IDENT_TOKEN */) {\n return { auto: true, order: 0 };\n }\n if (isNumberToken(token)) {\n return { auto: false, order: token.number };\n }\n throw new Error(\"Invalid z-index number parsed\");\n }\n};\n\nvar time = {\n name: 'time',\n parse: function (_context, value) {\n if (value.type === 15 /* DIMENSION_TOKEN */) {\n switch (value.unit.toLowerCase()) {\n case 's':\n return 1000 * value.number;\n case 'ms':\n return value.number;\n }\n }\n throw new Error(\"Unsupported time type\");\n }\n};\n\nvar opacity = {\n name: 'opacity',\n initialValue: '1',\n type: 0 /* VALUE */,\n prefix: false,\n parse: function (_context, token) {\n if (isNumberToken(token)) {\n return token.number;\n }\n return 1;\n }\n};\n\nvar textDecorationColor = {\n name: \"text-decoration-color\",\n initialValue: 'transparent',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'color'\n};\n\nvar textDecorationLine = {\n name: 'text-decoration-line',\n initialValue: 'none',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n return tokens\n .filter(isIdentToken)\n .map(function (token) {\n switch (token.value) {\n case 'underline':\n return 1 /* UNDERLINE */;\n case 'overline':\n return 2 /* OVERLINE */;\n case 'line-through':\n return 3 /* LINE_THROUGH */;\n case 'none':\n return 4 /* BLINK */;\n }\n return 0 /* NONE */;\n })\n .filter(function (line) { return line !== 0 /* NONE */; });\n }\n};\n\nvar fontFamily = {\n name: \"font-family\",\n initialValue: '',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n var accumulator = [];\n var results = [];\n tokens.forEach(function (token) {\n switch (token.type) {\n case 20 /* IDENT_TOKEN */:\n case 0 /* STRING_TOKEN */:\n accumulator.push(token.value);\n break;\n case 17 /* NUMBER_TOKEN */:\n accumulator.push(token.number.toString());\n break;\n case 4 /* COMMA_TOKEN */:\n results.push(accumulator.join(' '));\n accumulator.length = 0;\n break;\n }\n });\n if (accumulator.length) {\n results.push(accumulator.join(' '));\n }\n return results.map(function (result) { return (result.indexOf(' ') === -1 ? result : \"'\" + result + \"'\"); });\n }\n};\n\nvar fontSize = {\n name: \"font-size\",\n initialValue: '0',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'length'\n};\n\nvar fontWeight = {\n name: 'font-weight',\n initialValue: 'normal',\n type: 0 /* VALUE */,\n prefix: false,\n parse: function (_context, token) {\n if (isNumberToken(token)) {\n return token.number;\n }\n if (isIdentToken(token)) {\n switch (token.value) {\n case 'bold':\n return 700;\n case 'normal':\n default:\n return 400;\n }\n }\n return 400;\n }\n};\n\nvar fontVariant = {\n name: 'font-variant',\n initialValue: 'none',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (_context, tokens) {\n return tokens.filter(isIdentToken).map(function (token) { return token.value; });\n }\n};\n\nvar fontStyle = {\n name: 'font-style',\n initialValue: 'normal',\n prefix: false,\n type: 2 /* IDENT_VALUE */,\n parse: function (_context, overflow) {\n switch (overflow) {\n case 'oblique':\n return \"oblique\" /* OBLIQUE */;\n case 'italic':\n return \"italic\" /* ITALIC */;\n case 'normal':\n default:\n return \"normal\" /* NORMAL */;\n }\n }\n};\n\nvar contains = function (bit, value) { return (bit & value) !== 0; };\n\nvar content = {\n name: 'content',\n initialValue: 'none',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (_context, tokens) {\n if (tokens.length === 0) {\n return [];\n }\n var first = tokens[0];\n if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {\n return [];\n }\n return tokens;\n }\n};\n\nvar counterIncrement = {\n name: 'counter-increment',\n initialValue: 'none',\n prefix: true,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n if (tokens.length === 0) {\n return null;\n }\n var first = tokens[0];\n if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {\n return null;\n }\n var increments = [];\n var filtered = tokens.filter(nonWhiteSpace);\n for (var i = 0; i < filtered.length; i++) {\n var counter = filtered[i];\n var next = filtered[i + 1];\n if (counter.type === 20 /* IDENT_TOKEN */) {\n var increment = next && isNumberToken(next) ? next.number : 1;\n increments.push({ counter: counter.value, increment: increment });\n }\n }\n return increments;\n }\n};\n\nvar counterReset = {\n name: 'counter-reset',\n initialValue: 'none',\n prefix: true,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n if (tokens.length === 0) {\n return [];\n }\n var resets = [];\n var filtered = tokens.filter(nonWhiteSpace);\n for (var i = 0; i < filtered.length; i++) {\n var counter = filtered[i];\n var next = filtered[i + 1];\n if (isIdentToken(counter) && counter.value !== 'none') {\n var reset = next && isNumberToken(next) ? next.number : 0;\n resets.push({ counter: counter.value, reset: reset });\n }\n }\n return resets;\n }\n};\n\nvar duration = {\n name: 'duration',\n initialValue: '0s',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (context, tokens) {\n return tokens.filter(isDimensionToken).map(function (token) { return time.parse(context, token); });\n }\n};\n\nvar quotes = {\n name: 'quotes',\n initialValue: 'none',\n prefix: true,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n if (tokens.length === 0) {\n return null;\n }\n var first = tokens[0];\n if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {\n return null;\n }\n var quotes = [];\n var filtered = tokens.filter(isStringToken);\n if (filtered.length % 2 !== 0) {\n return null;\n }\n for (var i = 0; i < filtered.length; i += 2) {\n var open_1 = filtered[i].value;\n var close_1 = filtered[i + 1].value;\n quotes.push({ open: open_1, close: close_1 });\n }\n return quotes;\n }\n};\nvar getQuote = function (quotes, depth, open) {\n if (!quotes) {\n return '';\n }\n var quote = quotes[Math.min(depth, quotes.length - 1)];\n if (!quote) {\n return '';\n }\n return open ? quote.open : quote.close;\n};\n\nvar boxShadow = {\n name: 'box-shadow',\n initialValue: 'none',\n type: 1 /* LIST */,\n prefix: false,\n parse: function (context, tokens) {\n if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {\n return [];\n }\n return parseFunctionArgs(tokens).map(function (values) {\n var shadow = {\n color: 0x000000ff,\n offsetX: ZERO_LENGTH,\n offsetY: ZERO_LENGTH,\n blur: ZERO_LENGTH,\n spread: ZERO_LENGTH,\n inset: false\n };\n var c = 0;\n for (var i = 0; i < values.length; i++) {\n var token = values[i];\n if (isIdentWithValue(token, 'inset')) {\n shadow.inset = true;\n }\n else if (isLength(token)) {\n if (c === 0) {\n shadow.offsetX = token;\n }\n else if (c === 1) {\n shadow.offsetY = token;\n }\n else if (c === 2) {\n shadow.blur = token;\n }\n else {\n shadow.spread = token;\n }\n c++;\n }\n else {\n shadow.color = color$1.parse(context, token);\n }\n }\n return shadow;\n });\n }\n};\n\nvar paintOrder = {\n name: 'paint-order',\n initialValue: 'normal',\n prefix: false,\n type: 1 /* LIST */,\n parse: function (_context, tokens) {\n var DEFAULT_VALUE = [0 /* FILL */, 1 /* STROKE */, 2 /* MARKERS */];\n var layers = [];\n tokens.filter(isIdentToken).forEach(function (token) {\n switch (token.value) {\n case 'stroke':\n layers.push(1 /* STROKE */);\n break;\n case 'fill':\n layers.push(0 /* FILL */);\n break;\n case 'markers':\n layers.push(2 /* MARKERS */);\n break;\n }\n });\n DEFAULT_VALUE.forEach(function (value) {\n if (layers.indexOf(value) === -1) {\n layers.push(value);\n }\n });\n return layers;\n }\n};\n\nvar webkitTextStrokeColor = {\n name: \"-webkit-text-stroke-color\",\n initialValue: 'currentcolor',\n prefix: false,\n type: 3 /* TYPE_VALUE */,\n format: 'color'\n};\n\nvar webkitTextStrokeWidth = {\n name: \"-webkit-text-stroke-width\",\n initialValue: '0',\n type: 0 /* VALUE */,\n prefix: false,\n parse: function (_context, token) {\n if (isDimensionToken(token)) {\n return token.number;\n }\n return 0;\n }\n};\n\nvar CSSParsedDeclaration = /** @class */ (function () {\n function CSSParsedDeclaration(context, declaration) {\n var _a, _b;\n this.animationDuration = parse(context, duration, declaration.animationDuration);\n this.backgroundClip = parse(context, backgroundClip, declaration.backgroundClip);\n this.backgroundColor = parse(context, backgroundColor, declaration.backgroundColor);\n this.backgroundImage = parse(context, backgroundImage, declaration.backgroundImage);\n this.backgroundOrigin = parse(context, backgroundOrigin, declaration.backgroundOrigin);\n this.backgroundPosition = parse(context, backgroundPosition, declaration.backgroundPosition);\n this.backgroundRepeat = parse(context, backgroundRepeat, declaration.backgroundRepeat);\n this.backgroundSize = parse(context, backgroundSize, declaration.backgroundSize);\n this.borderTopColor = parse(context, borderTopColor, declaration.borderTopColor);\n this.borderRightColor = parse(context, borderRightColor, declaration.borderRightColor);\n this.borderBottomColor = parse(context, borderBottomColor, declaration.borderBottomColor);\n this.borderLeftColor = parse(context, borderLeftColor, declaration.borderLeftColor);\n this.borderTopLeftRadius = parse(context, borderTopLeftRadius, declaration.borderTopLeftRadius);\n this.borderTopRightRadius = parse(context, borderTopRightRadius, declaration.borderTopRightRadius);\n this.borderBottomRightRadius = parse(context, borderBottomRightRadius, declaration.borderBottomRightRadius);\n this.borderBottomLeftRadius = parse(context, borderBottomLeftRadius, declaration.borderBottomLeftRadius);\n this.borderTopStyle = parse(context, borderTopStyle, declaration.borderTopStyle);\n this.borderRightStyle = parse(context, borderRightStyle, declaration.borderRightStyle);\n this.borderBottomStyle = parse(context, borderBottomStyle, declaration.borderBottomStyle);\n this.borderLeftStyle = parse(context, borderLeftStyle, declaration.borderLeftStyle);\n this.borderTopWidth = parse(context, borderTopWidth, declaration.borderTopWidth);\n this.borderRightWidth = parse(context, borderRightWidth, declaration.borderRightWidth);\n this.borderBottomWidth = parse(context, borderBottomWidth, declaration.borderBottomWidth);\n this.borderLeftWidth = parse(context, borderLeftWidth, declaration.borderLeftWidth);\n this.boxShadow = parse(context, boxShadow, declaration.boxShadow);\n this.color = parse(context, color, declaration.color);\n this.direction = parse(context, direction, declaration.direction);\n this.display = parse(context, display, declaration.display);\n this.float = parse(context, float, declaration.cssFloat);\n this.fontFamily = parse(context, fontFamily, declaration.fontFamily);\n this.fontSize = parse(context, fontSize, declaration.fontSize);\n this.fontStyle = parse(context, fontStyle, declaration.fontStyle);\n this.fontVariant = parse(context, fontVariant, declaration.fontVariant);\n this.fontWeight = parse(context, fontWeight, declaration.fontWeight);\n this.letterSpacing = parse(context, letterSpacing, declaration.letterSpacing);\n this.lineBreak = parse(context, lineBreak, declaration.lineBreak);\n this.lineHeight = parse(context, lineHeight, declaration.lineHeight);\n this.listStyleImage = parse(context, listStyleImage, declaration.listStyleImage);\n this.listStylePosition = parse(context, listStylePosition, declaration.listStylePosition);\n this.listStyleType = parse(context, listStyleType, declaration.listStyleType);\n this.marginTop = parse(context, marginTop, declaration.marginTop);\n this.marginRight = parse(context, marginRight, declaration.marginRight);\n this.marginBottom = parse(context, marginBottom, declaration.marginBottom);\n this.marginLeft = parse(context, marginLeft, declaration.marginLeft);\n this.opacity = parse(context, opacity, declaration.opacity);\n var overflowTuple = parse(context, overflow, declaration.overflow);\n this.overflowX = overflowTuple[0];\n this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];\n this.overflowWrap = parse(context, overflowWrap, declaration.overflowWrap);\n this.paddingTop = parse(context, paddingTop, declaration.paddingTop);\n this.paddingRight = parse(context, paddingRight, declaration.paddingRight);\n this.paddingBottom = parse(context, paddingBottom, declaration.paddingBottom);\n this.paddingLeft = parse(context, paddingLeft, declaration.paddingLeft);\n this.paintOrder = parse(context, paintOrder, declaration.paintOrder);\n this.position = parse(context, position, declaration.position);\n this.textAlign = parse(context, textAlign, declaration.textAlign);\n this.textDecorationColor = parse(context, textDecorationColor, (_a = declaration.textDecorationColor) !== null && _a !== void 0 ? _a : declaration.color);\n this.textDecorationLine = parse(context, textDecorationLine, (_b = declaration.textDecorationLine) !== null && _b !== void 0 ? _b : declaration.textDecoration);\n this.textShadow = parse(context, textShadow, declaration.textShadow);\n this.textTransform = parse(context, textTransform, declaration.textTransform);\n this.transform = parse(context, transform$1, declaration.transform);\n this.transformOrigin = parse(context, transformOrigin, declaration.transformOrigin);\n this.visibility = parse(context, visibility, declaration.visibility);\n this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);\n this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);\n this.wordBreak = parse(context, wordBreak, declaration.wordBreak);\n this.zIndex = parse(context, zIndex, declaration.zIndex);\n }\n CSSParsedDeclaration.prototype.isVisible = function () {\n return this.display > 0 && this.opacity > 0 && this.visibility === 0 /* VISIBLE */;\n };\n CSSParsedDeclaration.prototype.isTransparent = function () {\n return isTransparent(this.backgroundColor);\n };\n CSSParsedDeclaration.prototype.isTransformed = function () {\n return this.transform !== null;\n };\n CSSParsedDeclaration.prototype.isPositioned = function () {\n return this.position !== 0 /* STATIC */;\n };\n CSSParsedDeclaration.prototype.isPositionedWithZIndex = function () {\n return this.isPositioned() && !this.zIndex.auto;\n };\n CSSParsedDeclaration.prototype.isFloating = function () {\n return this.float !== 0 /* NONE */;\n };\n CSSParsedDeclaration.prototype.isInlineLevel = function () {\n return (contains(this.display, 4 /* INLINE */) ||\n contains(this.display, 33554432 /* INLINE_BLOCK */) ||\n contains(this.display, 268435456 /* INLINE_FLEX */) ||\n contains(this.display, 536870912 /* INLINE_GRID */) ||\n contains(this.display, 67108864 /* INLINE_LIST_ITEM */) ||\n contains(this.display, 134217728 /* INLINE_TABLE */));\n };\n return CSSParsedDeclaration;\n}());\nvar CSSParsedPseudoDeclaration = /** @class */ (function () {\n function CSSParsedPseudoDeclaration(context, declaration) {\n this.content = parse(context, content, declaration.content);\n this.quotes = parse(context, quotes, declaration.quotes);\n }\n return CSSParsedPseudoDeclaration;\n}());\nvar CSSParsedCounterDeclaration = /** @class */ (function () {\n function CSSParsedCounterDeclaration(context, declaration) {\n this.counterIncrement = parse(context, counterIncrement, declaration.counterIncrement);\n this.counterReset = parse(context, counterReset, declaration.counterReset);\n }\n return CSSParsedCounterDeclaration;\n}());\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nvar parse = function (context, descriptor, style) {\n var tokenizer = new Tokenizer();\n var value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;\n tokenizer.write(value);\n var parser = new Parser(tokenizer.read());\n switch (descriptor.type) {\n case 2 /* IDENT_VALUE */:\n var token = parser.parseComponentValue();\n return descriptor.parse(context, isIdentToken(token) ? token.value : descriptor.initialValue);\n case 0 /* VALUE */:\n return descriptor.parse(context, parser.parseComponentValue());\n case 1 /* LIST */:\n return descriptor.parse(context, parser.parseComponentValues());\n case 4 /* TOKEN_VALUE */:\n return parser.parseComponentValue();\n case 3 /* TYPE_VALUE */:\n switch (descriptor.format) {\n case 'angle':\n return angle.parse(context, parser.parseComponentValue());\n case 'color':\n return color$1.parse(context, parser.parseComponentValue());\n case 'image':\n return image.parse(context, parser.parseComponentValue());\n case 'length':\n var length_1 = parser.parseComponentValue();\n return isLength(length_1) ? length_1 : ZERO_LENGTH;\n case 'length-percentage':\n var value_1 = parser.parseComponentValue();\n return isLengthPercentage(value_1) ? value_1 : ZERO_LENGTH;\n case 'time':\n return time.parse(context, parser.parseComponentValue());\n }\n break;\n }\n};\n\nvar elementDebuggerAttribute = 'data-html2canvas-debug';\nvar getElementDebugType = function (element) {\n var attribute = element.getAttribute(elementDebuggerAttribute);\n switch (attribute) {\n case 'all':\n return 1 /* ALL */;\n case 'clone':\n return 2 /* CLONE */;\n case 'parse':\n return 3 /* PARSE */;\n case 'render':\n return 4 /* RENDER */;\n default:\n return 0 /* NONE */;\n }\n};\nvar isDebugging = function (element, type) {\n var elementType = getElementDebugType(element);\n return elementType === 1 /* ALL */ || type === elementType;\n};\n\nvar ElementContainer = /** @class */ (function () {\n function ElementContainer(context, element) {\n this.context = context;\n this.textNodes = [];\n this.elements = [];\n this.flags = 0;\n if (isDebugging(element, 3 /* PARSE */)) {\n debugger;\n }\n this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));\n if (isHTMLElementNode(element)) {\n if (this.styles.animationDuration.some(function (duration) { return duration > 0; })) {\n element.style.animationDuration = '0s';\n }\n if (this.styles.transform !== null) {\n // getBoundingClientRect takes transforms into account\n element.style.transform = 'none';\n }\n }\n this.bounds = parseBounds(this.context, element);\n if (isDebugging(element, 4 /* RENDER */)) {\n this.flags |= 16 /* DEBUG_RENDER */;\n }\n }\n return ElementContainer;\n}());\n\n/*\n * text-segmentation 1.0.3 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar base64 = 'AAAAAAAAAAAAEA4AGBkAAFAaAAACAAAAAAAIABAAGAAwADgACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAQABIAEQATAAIABAACAAQAAgAEAAIABAAVABcAAgAEAAIABAACAAQAGAAaABwAHgAgACIAI4AlgAIABAAmwCjAKgAsAC2AL4AvQDFAMoA0gBPAVYBWgEIAAgACACMANoAYgFkAWwBdAF8AX0BhQGNAZUBlgGeAaMBlQGWAasBswF8AbsBwwF0AcsBYwHTAQgA2wG/AOMBdAF8AekB8QF0AfkB+wHiAHQBfAEIAAMC5gQIAAsCEgIIAAgAFgIeAggAIgIpAggAMQI5AkACygEIAAgASAJQAlgCYAIIAAgACAAKBQoFCgUTBRMFGQUrBSsFCAAIAAgACAAIAAgACAAIAAgACABdAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABoAmgCrwGvAQgAbgJ2AggAHgEIAAgACADnAXsCCAAIAAgAgwIIAAgACAAIAAgACACKAggAkQKZAggAPADJAAgAoQKkAqwCsgK6AsICCADJAggA0AIIAAgACAAIANYC3gIIAAgACAAIAAgACABAAOYCCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAkASoB+QIEAAgACAA8AEMCCABCBQgACABJBVAFCAAIAAgACAAIAAgACAAIAAgACABTBVoFCAAIAFoFCABfBWUFCAAIAAgACAAIAAgAbQUIAAgACAAIAAgACABzBXsFfQWFBYoFigWKBZEFigWKBYoFmAWfBaYFrgWxBbkFCAAIAAgACAAIAAgACAAIAAgACAAIAMEFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAMgFCADQBQgACAAIAAgACAAIAAgACAAIAAgACAAIAO4CCAAIAAgAiQAIAAgACABAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAD0AggACAD8AggACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIANYFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAMDvwAIAAgAJAIIAAgACAAIAAgACAAIAAgACwMTAwgACAB9BOsEGwMjAwgAKwMyAwsFYgE3A/MEPwMIAEUDTQNRAwgAWQOsAGEDCAAIAAgACAAIAAgACABpAzQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFIQUoBSwFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABtAwgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABMAEwACAAIAAgACAAIABgACAAIAAgACAC/AAgACAAyAQgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACAAIAAwAAgACAAIAAgACAAIAAgACAAIAAAARABIAAgACAAIABQASAAIAAgAIABwAEAAjgCIABsAqAC2AL0AigDQAtwC+IJIQqVAZUBWQqVAZUBlQGVAZUBlQGrC5UBlQGVAZUBlQGVAZUBlQGVAXsKlQGVAbAK6wsrDGUMpQzlDJUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAfAKAAuZA64AtwCJALoC6ADwAAgAuACgA/oEpgO6AqsD+AAIAAgAswMIAAgACAAIAIkAuwP5AfsBwwPLAwgACAAIAAgACADRA9kDCAAIAOED6QMIAAgACAAIAAgACADuA/YDCAAIAP4DyQAIAAgABgQIAAgAXQAOBAgACAAIAAgACAAIABMECAAIAAgACAAIAAgACAD8AAQBCAAIAAgAGgQiBCoECAExBAgAEAEIAAgACAAIAAgACAAIAAgACAAIAAgACAA4BAgACABABEYECAAIAAgATAQYAQgAVAQIAAgACAAIAAgACAAIAAgACAAIAFoECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAOQEIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAB+BAcACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAEABhgSMBAgACAAIAAgAlAQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAwAEAAQABAADAAMAAwADAAQABAAEAAQABAAEAAQABHATAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAdQMIAAgACAAIAAgACAAIAMkACAAIAAgAfQMIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACFA4kDCAAIAAgACAAIAOcBCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAIcDCAAIAAgACAAIAAgACAAIAAgACAAIAJEDCAAIAAgACADFAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABgBAgAZgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAbAQCBXIECAAIAHkECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABAAJwEQACjBKoEsgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAC6BMIECAAIAAgACAAIAAgACABmBAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAxwQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAGYECAAIAAgAzgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBd0FXwUIAOIF6gXxBYoF3gT5BQAGCAaKBYoFigWKBYoFigWKBYoFigWKBYoFigXWBIoFigWKBYoFigWKBYoFigWKBYsFEAaKBYoFigWKBYoFigWKBRQGCACKBYoFigWKBQgACAAIANEECAAIABgGigUgBggAJgYIAC4GMwaKBYoF0wQ3Bj4GigWKBYoFigWKBYoFigWKBYoFigWKBYoFigUIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWLBf///////wQABAAEAAQABAAEAAQABAAEAAQAAwAEAAQAAgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAQADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUAAAAFAAUAAAAFAAUAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAQAAAAUABQAFAAUABQAFAAAAAAAFAAUAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAUAAQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAAABwAHAAcAAAAHAAcABwAFAAEAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAcABwAFAAUAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQABAAAAAAAAAAAAAAAFAAUABQAFAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAHAAcAAAAHAAcAAAAAAAUABQAHAAUAAQAHAAEABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwABAAUABQAFAAUAAAAAAAAAAAAAAAEAAQABAAEAAQABAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABQANAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAABQAHAAUABQAFAAAAAAAAAAcABQAFAAUABQAFAAQABAAEAAQABAAEAAQABAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUAAAAFAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAUAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAcABwAFAAcABwAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUABwAHAAUABQAFAAUAAAAAAAcABwAAAAAABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAAAAAAAAAAABQAFAAAAAAAFAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAFAAUABQAFAAUAAAAFAAUABwAAAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABwAFAAUABQAFAAAAAAAHAAcAAAAAAAcABwAFAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAAAAAAAAAHAAcABwAAAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAUABQAFAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAHAAcABQAHAAcAAAAFAAcABwAAAAcABwAFAAUAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAFAAcABwAFAAUABQAAAAUAAAAHAAcABwAHAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAHAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAUAAAAFAAUAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABwAFAAUABQAFAAUABQAAAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABQAFAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAFAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAHAAUABQAFAAUABQAFAAUABwAHAAcABwAHAAcABwAHAAUABwAHAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABwAHAAcABwAFAAUABwAHAAcAAAAAAAAAAAAHAAcABQAHAAcABwAHAAcABwAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAUABQAFAAUABQAFAAUAAAAFAAAABQAAAAAABQAFAAUABQAFAAUABQAFAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAUABQAFAAUABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABwAFAAcABwAHAAcABwAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAUABQAFAAUABwAHAAUABQAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABQAFAAcABwAHAAUABwAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAcABQAFAAUABQAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAAAAAABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAUABQAHAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAFAAUABQAFAAcABwAFAAUABwAHAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAcABwAFAAUABwAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABQAAAAAABQAFAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAcABwAAAAAAAAAAAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAcABwAFAAcABwAAAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAFAAUABQAAAAUABQAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABwAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAHAAcABQAHAAUABQAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAAABwAHAAAAAAAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAFAAUABwAFAAcABwAFAAcABQAFAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAAAAAABwAHAAcABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAFAAcABwAFAAUABQAFAAUABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAUABQAFAAcABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABQAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAAAAAAFAAUABwAHAAcABwAFAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAHAAUABQAFAAUABQAFAAUABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAABQAAAAUABQAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAHAAcAAAAFAAUAAAAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABQAFAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAABQAFAAUABQAFAAUABQAAAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAFAAUABQAFAAUADgAOAA4ADgAOAA4ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAMAAwADAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAAAAAAAAAAAAsADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwACwAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAADgAOAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAAAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4AAAAOAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAAAAAAAAAAAA4AAAAOAAAAAAAAAAAADgAOAA4AAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAA=';\n\n/*\n * utrie 1.0.2 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar chars$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nvar lookup$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (var i$1 = 0; i$1 < chars$1.length; i$1++) {\n lookup$1[chars$1.charCodeAt(i$1)] = i$1;\n}\nvar decode = function (base64) {\n var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;\n if (base64[base64.length - 1] === '=') {\n bufferLength--;\n if (base64[base64.length - 2] === '=') {\n bufferLength--;\n }\n }\n var buffer = typeof ArrayBuffer !== 'undefined' &&\n typeof Uint8Array !== 'undefined' &&\n typeof Uint8Array.prototype.slice !== 'undefined'\n ? new ArrayBuffer(bufferLength)\n : new Array(bufferLength);\n var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer);\n for (i = 0; i < len; i += 4) {\n encoded1 = lookup$1[base64.charCodeAt(i)];\n encoded2 = lookup$1[base64.charCodeAt(i + 1)];\n encoded3 = lookup$1[base64.charCodeAt(i + 2)];\n encoded4 = lookup$1[base64.charCodeAt(i + 3)];\n bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);\n bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);\n bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);\n }\n return buffer;\n};\nvar polyUint16Array = function (buffer) {\n var length = buffer.length;\n var bytes = [];\n for (var i = 0; i < length; i += 2) {\n bytes.push((buffer[i + 1] << 8) | buffer[i]);\n }\n return bytes;\n};\nvar polyUint32Array = function (buffer) {\n var length = buffer.length;\n var bytes = [];\n for (var i = 0; i < length; i += 4) {\n bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]);\n }\n return bytes;\n};\n\n/** Shift size for getting the index-2 table offset. */\nvar UTRIE2_SHIFT_2 = 5;\n/** Shift size for getting the index-1 table offset. */\nvar UTRIE2_SHIFT_1 = 6 + 5;\n/**\n * Shift size for shifting left the index array values.\n * Increases possible data size with 16-bit index values at the cost\n * of compactability.\n * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY.\n */\nvar UTRIE2_INDEX_SHIFT = 2;\n/**\n * Difference between the two shift sizes,\n * for getting an index-1 offset from an index-2 offset. 6=11-5\n */\nvar UTRIE2_SHIFT_1_2 = UTRIE2_SHIFT_1 - UTRIE2_SHIFT_2;\n/**\n * The part of the index-2 table for U+D800..U+DBFF stores values for\n * lead surrogate code _units_ not code _points_.\n * Values for lead surrogate code _points_ are indexed with this portion of the table.\n * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.)\n */\nvar UTRIE2_LSCP_INDEX_2_OFFSET = 0x10000 >> UTRIE2_SHIFT_2;\n/** Number of entries in a data block. 32=0x20 */\nvar UTRIE2_DATA_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_2;\n/** Mask for getting the lower bits for the in-data-block offset. */\nvar UTRIE2_DATA_MASK = UTRIE2_DATA_BLOCK_LENGTH - 1;\nvar UTRIE2_LSCP_INDEX_2_LENGTH = 0x400 >> UTRIE2_SHIFT_2;\n/** Count the lengths of both BMP pieces. 2080=0x820 */\nvar UTRIE2_INDEX_2_BMP_LENGTH = UTRIE2_LSCP_INDEX_2_OFFSET + UTRIE2_LSCP_INDEX_2_LENGTH;\n/**\n * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.\n * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2.\n */\nvar UTRIE2_UTF8_2B_INDEX_2_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH;\nvar UTRIE2_UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */\n/**\n * The index-1 table, only used for supplementary code points, at offset 2112=0x840.\n * Variable length, for code points up to highStart, where the last single-value range starts.\n * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1.\n * (For 0x100000 supplementary code points U+10000..U+10ffff.)\n *\n * The part of the index-2 table for supplementary code points starts\n * after this index-1 table.\n *\n * Both the index-1 table and the following part of the index-2 table\n * are omitted completely if there is only BMP data.\n */\nvar UTRIE2_INDEX_1_OFFSET = UTRIE2_UTF8_2B_INDEX_2_OFFSET + UTRIE2_UTF8_2B_INDEX_2_LENGTH;\n/**\n * Number of index-1 entries for the BMP. 32=0x20\n * This part of the index-1 table is omitted from the serialized form.\n */\nvar UTRIE2_OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> UTRIE2_SHIFT_1;\n/** Number of entries in an index-2 block. 64=0x40 */\nvar UTRIE2_INDEX_2_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_1_2;\n/** Mask for getting the lower bits for the in-index-2-block offset. */\nvar UTRIE2_INDEX_2_MASK = UTRIE2_INDEX_2_BLOCK_LENGTH - 1;\nvar slice16 = function (view, start, end) {\n if (view.slice) {\n return view.slice(start, end);\n }\n return new Uint16Array(Array.prototype.slice.call(view, start, end));\n};\nvar slice32 = function (view, start, end) {\n if (view.slice) {\n return view.slice(start, end);\n }\n return new Uint32Array(Array.prototype.slice.call(view, start, end));\n};\nvar createTrieFromBase64 = function (base64, _byteLength) {\n var buffer = decode(base64);\n var view32 = Array.isArray(buffer) ? polyUint32Array(buffer) : new Uint32Array(buffer);\n var view16 = Array.isArray(buffer) ? polyUint16Array(buffer) : new Uint16Array(buffer);\n var headerLength = 24;\n var index = slice16(view16, headerLength / 2, view32[4] / 2);\n var data = view32[5] === 2\n ? slice16(view16, (headerLength + view32[4]) / 2)\n : slice32(view32, Math.ceil((headerLength + view32[4]) / 4));\n return new Trie(view32[0], view32[1], view32[2], view32[3], index, data);\n};\nvar Trie = /** @class */ (function () {\n function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) {\n this.initialValue = initialValue;\n this.errorValue = errorValue;\n this.highStart = highStart;\n this.highValueIndex = highValueIndex;\n this.index = index;\n this.data = data;\n }\n /**\n * Get the value for a code point as stored in the Trie.\n *\n * @param codePoint the code point\n * @return the value\n */\n Trie.prototype.get = function (codePoint) {\n var ix;\n if (codePoint >= 0) {\n if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) {\n // Ordinary BMP code point, excluding leading surrogates.\n // BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index.\n // 16 bit data is stored in the index array itself.\n ix = this.index[codePoint >> UTRIE2_SHIFT_2];\n ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);\n return this.data[ix];\n }\n if (codePoint <= 0xffff) {\n // Lead Surrogate Code Point. A Separate index section is stored for\n // lead surrogate code units and code points.\n // The main index has the code unit data.\n // For this function, we need the code point data.\n // Note: this expression could be refactored for slightly improved efficiency, but\n // surrogate code points will be so rare in practice that it's not worth it.\n ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2)];\n ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);\n return this.data[ix];\n }\n if (codePoint < this.highStart) {\n // Supplemental code point, use two-level lookup.\n ix = UTRIE2_INDEX_1_OFFSET - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH + (codePoint >> UTRIE2_SHIFT_1);\n ix = this.index[ix];\n ix += (codePoint >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK;\n ix = this.index[ix];\n ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);\n return this.data[ix];\n }\n if (codePoint <= 0x10ffff) {\n return this.data[this.highValueIndex];\n }\n }\n // Fall through. The code point is outside of the legal range of 0..0x10ffff.\n return this.errorValue;\n };\n return Trie;\n}());\n\n/*\n * base64-arraybuffer 1.0.2 \n * Copyright (c) 2022 Niklas von Hertzen \n * Released under MIT License\n */\nvar chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nvar lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (var i = 0; i < chars.length; i++) {\n lookup[chars.charCodeAt(i)] = i;\n}\n\nvar Prepend = 1;\nvar CR = 2;\nvar LF = 3;\nvar Control = 4;\nvar Extend = 5;\nvar SpacingMark = 7;\nvar L = 8;\nvar V = 9;\nvar T = 10;\nvar LV = 11;\nvar LVT = 12;\nvar ZWJ = 13;\nvar Extended_Pictographic = 14;\nvar RI = 15;\nvar toCodePoints = function (str) {\n var codePoints = [];\n var i = 0;\n var length = str.length;\n while (i < length) {\n var value = str.charCodeAt(i++);\n if (value >= 0xd800 && value <= 0xdbff && i < length) {\n var extra = str.charCodeAt(i++);\n if ((extra & 0xfc00) === 0xdc00) {\n codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);\n }\n else {\n codePoints.push(value);\n i--;\n }\n }\n else {\n codePoints.push(value);\n }\n }\n return codePoints;\n};\nvar fromCodePoint = function () {\n var codePoints = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n codePoints[_i] = arguments[_i];\n }\n if (String.fromCodePoint) {\n return String.fromCodePoint.apply(String, codePoints);\n }\n var length = codePoints.length;\n if (!length) {\n return '';\n }\n var codeUnits = [];\n var index = -1;\n var result = '';\n while (++index < length) {\n var codePoint = codePoints[index];\n if (codePoint <= 0xffff) {\n codeUnits.push(codePoint);\n }\n else {\n codePoint -= 0x10000;\n codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00);\n }\n if (index + 1 === length || codeUnits.length > 0x4000) {\n result += String.fromCharCode.apply(String, codeUnits);\n codeUnits.length = 0;\n }\n }\n return result;\n};\nvar UnicodeTrie = createTrieFromBase64(base64);\nvar BREAK_NOT_ALLOWED = '×';\nvar BREAK_ALLOWED = '÷';\nvar codePointToClass = function (codePoint) { return UnicodeTrie.get(codePoint); };\nvar _graphemeBreakAtIndex = function (_codePoints, classTypes, index) {\n var prevIndex = index - 2;\n var prev = classTypes[prevIndex];\n var current = classTypes[index - 1];\n var next = classTypes[index];\n // GB3 Do not break between a CR and LF\n if (current === CR && next === LF) {\n return BREAK_NOT_ALLOWED;\n }\n // GB4 Otherwise, break before and after controls.\n if (current === CR || current === LF || current === Control) {\n return BREAK_ALLOWED;\n }\n // GB5\n if (next === CR || next === LF || next === Control) {\n return BREAK_ALLOWED;\n }\n // Do not break Hangul syllable sequences.\n // GB6\n if (current === L && [L, V, LV, LVT].indexOf(next) !== -1) {\n return BREAK_NOT_ALLOWED;\n }\n // GB7\n if ((current === LV || current === V) && (next === V || next === T)) {\n return BREAK_NOT_ALLOWED;\n }\n // GB8\n if ((current === LVT || current === T) && next === T) {\n return BREAK_NOT_ALLOWED;\n }\n // GB9 Do not break before extending characters or ZWJ.\n if (next === ZWJ || next === Extend) {\n return BREAK_NOT_ALLOWED;\n }\n // Do not break before SpacingMarks, or after Prepend characters.\n // GB9a\n if (next === SpacingMark) {\n return BREAK_NOT_ALLOWED;\n }\n // GB9a\n if (current === Prepend) {\n return BREAK_NOT_ALLOWED;\n }\n // GB11 Do not break within emoji modifier sequences or emoji zwj sequences.\n if (current === ZWJ && next === Extended_Pictographic) {\n while (prev === Extend) {\n prev = classTypes[--prevIndex];\n }\n if (prev === Extended_Pictographic) {\n return BREAK_NOT_ALLOWED;\n }\n }\n // GB12 Do not break within emoji flag sequences.\n // That is, do not break between regional indicator (RI) symbols\n // if there is an odd number of RI characters before the break point.\n if (current === RI && next === RI) {\n var countRI = 0;\n while (prev === RI) {\n countRI++;\n prev = classTypes[--prevIndex];\n }\n if (countRI % 2 === 0) {\n return BREAK_NOT_ALLOWED;\n }\n }\n return BREAK_ALLOWED;\n};\nvar GraphemeBreaker = function (str) {\n var codePoints = toCodePoints(str);\n var length = codePoints.length;\n var index = 0;\n var lastEnd = 0;\n var classTypes = codePoints.map(codePointToClass);\n return {\n next: function () {\n if (index >= length) {\n return { done: true, value: null };\n }\n var graphemeBreak = BREAK_NOT_ALLOWED;\n while (index < length &&\n (graphemeBreak = _graphemeBreakAtIndex(codePoints, classTypes, ++index)) === BREAK_NOT_ALLOWED) { }\n if (graphemeBreak !== BREAK_NOT_ALLOWED || index === length) {\n var value = fromCodePoint.apply(null, codePoints.slice(lastEnd, index));\n lastEnd = index;\n return { value: value, done: false };\n }\n return { done: true, value: null };\n },\n };\n};\nvar splitGraphemes = function (str) {\n var breaker = GraphemeBreaker(str);\n var graphemes = [];\n var bk;\n while (!(bk = breaker.next()).done) {\n if (bk.value) {\n graphemes.push(bk.value.slice());\n }\n }\n return graphemes;\n};\n\nvar testRangeBounds = function (document) {\n var TEST_HEIGHT = 123;\n if (document.createRange) {\n var range = document.createRange();\n if (range.getBoundingClientRect) {\n var testElement = document.createElement('boundtest');\n testElement.style.height = TEST_HEIGHT + \"px\";\n testElement.style.display = 'block';\n document.body.appendChild(testElement);\n range.selectNode(testElement);\n var rangeBounds = range.getBoundingClientRect();\n var rangeHeight = Math.round(rangeBounds.height);\n document.body.removeChild(testElement);\n if (rangeHeight === TEST_HEIGHT) {\n return true;\n }\n }\n }\n return false;\n};\nvar testIOSLineBreak = function (document) {\n var testElement = document.createElement('boundtest');\n testElement.style.width = '50px';\n testElement.style.display = 'block';\n testElement.style.fontSize = '12px';\n testElement.style.letterSpacing = '0px';\n testElement.style.wordSpacing = '0px';\n document.body.appendChild(testElement);\n var range = document.createRange();\n testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';\n var node = testElement.firstChild;\n var textList = toCodePoints$1(node.data).map(function (i) { return fromCodePoint$1(i); });\n var offset = 0;\n var prev = {};\n // ios 13 does not handle range getBoundingClientRect line changes correctly #2177\n var supports = textList.every(function (text, i) {\n range.setStart(node, offset);\n range.setEnd(node, offset + text.length);\n var rect = range.getBoundingClientRect();\n offset += text.length;\n var boundAhead = rect.x > prev.x || rect.y > prev.y;\n prev = rect;\n if (i === 0) {\n return true;\n }\n return boundAhead;\n });\n document.body.removeChild(testElement);\n return supports;\n};\nvar testCORS = function () { return typeof new Image().crossOrigin !== 'undefined'; };\nvar testResponseType = function () { return typeof new XMLHttpRequest().responseType === 'string'; };\nvar testSVG = function (document) {\n var img = new Image();\n var canvas = document.createElement('canvas');\n var ctx = canvas.getContext('2d');\n if (!ctx) {\n return false;\n }\n img.src = \"data:image/svg+xml,\";\n try {\n ctx.drawImage(img, 0, 0);\n canvas.toDataURL();\n }\n catch (e) {\n return false;\n }\n return true;\n};\nvar isGreenPixel = function (data) {\n return data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;\n};\nvar testForeignObject = function (document) {\n var canvas = document.createElement('canvas');\n var size = 100;\n canvas.width = size;\n canvas.height = size;\n var ctx = canvas.getContext('2d');\n if (!ctx) {\n return Promise.reject(false);\n }\n ctx.fillStyle = 'rgb(0, 255, 0)';\n ctx.fillRect(0, 0, size, size);\n var img = new Image();\n var greenImageSrc = canvas.toDataURL();\n img.src = greenImageSrc;\n var svg = createForeignObjectSVG(size, size, 0, 0, img);\n ctx.fillStyle = 'red';\n ctx.fillRect(0, 0, size, size);\n return loadSerializedSVG$1(svg)\n .then(function (img) {\n ctx.drawImage(img, 0, 0);\n var data = ctx.getImageData(0, 0, size, size).data;\n ctx.fillStyle = 'red';\n ctx.fillRect(0, 0, size, size);\n var node = document.createElement('div');\n node.style.backgroundImage = \"url(\" + greenImageSrc + \")\";\n node.style.height = size + \"px\";\n // Firefox 55 does not render inline tags\n return isGreenPixel(data)\n ? loadSerializedSVG$1(createForeignObjectSVG(size, size, 0, 0, node))\n : Promise.reject(false);\n })\n .then(function (img) {\n ctx.drawImage(img, 0, 0);\n // Edge does not render background-images\n return isGreenPixel(ctx.getImageData(0, 0, size, size).data);\n })\n .catch(function () { return false; });\n};\nvar createForeignObjectSVG = function (width, height, x, y, node) {\n var xmlns = 'http://www.w3.org/2000/svg';\n var svg = document.createElementNS(xmlns, 'svg');\n var foreignObject = document.createElementNS(xmlns, 'foreignObject');\n svg.setAttributeNS(null, 'width', width.toString());\n svg.setAttributeNS(null, 'height', height.toString());\n foreignObject.setAttributeNS(null, 'width', '100%');\n foreignObject.setAttributeNS(null, 'height', '100%');\n foreignObject.setAttributeNS(null, 'x', x.toString());\n foreignObject.setAttributeNS(null, 'y', y.toString());\n foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');\n svg.appendChild(foreignObject);\n foreignObject.appendChild(node);\n return svg;\n};\nvar loadSerializedSVG$1 = function (svg) {\n return new Promise(function (resolve, reject) {\n var img = new Image();\n img.onload = function () { return resolve(img); };\n img.onerror = reject;\n img.src = \"data:image/svg+xml;charset=utf-8,\" + encodeURIComponent(new XMLSerializer().serializeToString(svg));\n });\n};\nvar FEATURES = {\n get SUPPORT_RANGE_BOUNDS() {\n var value = testRangeBounds(document);\n Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', { value: value });\n return value;\n },\n get SUPPORT_WORD_BREAKING() {\n var value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);\n Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', { value: value });\n return value;\n },\n get SUPPORT_SVG_DRAWING() {\n var value = testSVG(document);\n Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', { value: value });\n return value;\n },\n get SUPPORT_FOREIGNOBJECT_DRAWING() {\n var value = typeof Array.from === 'function' && typeof window.fetch === 'function'\n ? testForeignObject(document)\n : Promise.resolve(false);\n Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', { value: value });\n return value;\n },\n get SUPPORT_CORS_IMAGES() {\n var value = testCORS();\n Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', { value: value });\n return value;\n },\n get SUPPORT_RESPONSE_TYPE() {\n var value = testResponseType();\n Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', { value: value });\n return value;\n },\n get SUPPORT_CORS_XHR() {\n var value = 'withCredentials' in new XMLHttpRequest();\n Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', { value: value });\n return value;\n },\n get SUPPORT_NATIVE_TEXT_SEGMENTATION() {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n var value = !!(typeof Intl !== 'undefined' && Intl.Segmenter);\n Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', { value: value });\n return value;\n }\n};\n\nvar TextBounds = /** @class */ (function () {\n function TextBounds(text, bounds) {\n this.text = text;\n this.bounds = bounds;\n }\n return TextBounds;\n}());\nvar parseTextBounds = function (context, value, styles, node) {\n var textList = breakText(value, styles);\n var textBounds = [];\n var offset = 0;\n textList.forEach(function (text) {\n if (styles.textDecorationLine.length || text.trim().length > 0) {\n if (FEATURES.SUPPORT_RANGE_BOUNDS) {\n var clientRects = createRange(node, offset, text.length).getClientRects();\n if (clientRects.length > 1) {\n var subSegments = segmentGraphemes(text);\n var subOffset_1 = 0;\n subSegments.forEach(function (subSegment) {\n textBounds.push(new TextBounds(subSegment, Bounds.fromDOMRectList(context, createRange(node, subOffset_1 + offset, subSegment.length).getClientRects())));\n subOffset_1 += subSegment.length;\n });\n }\n else {\n textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));\n }\n }\n else {\n var replacementNode = node.splitText(text.length);\n textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));\n node = replacementNode;\n }\n }\n else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {\n node = node.splitText(text.length);\n }\n offset += text.length;\n });\n return textBounds;\n};\nvar getWrapperBounds = function (context, node) {\n var ownerDocument = node.ownerDocument;\n if (ownerDocument) {\n var wrapper = ownerDocument.createElement('html2canvaswrapper');\n wrapper.appendChild(node.cloneNode(true));\n var parentNode = node.parentNode;\n if (parentNode) {\n parentNode.replaceChild(wrapper, node);\n var bounds = parseBounds(context, wrapper);\n if (wrapper.firstChild) {\n parentNode.replaceChild(wrapper.firstChild, wrapper);\n }\n return bounds;\n }\n }\n return Bounds.EMPTY;\n};\nvar createRange = function (node, offset, length) {\n var ownerDocument = node.ownerDocument;\n if (!ownerDocument) {\n throw new Error('Node has no owner document');\n }\n var range = ownerDocument.createRange();\n range.setStart(node, offset);\n range.setEnd(node, offset + length);\n return range;\n};\nvar segmentGraphemes = function (value) {\n if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n var segmenter = new Intl.Segmenter(void 0, { granularity: 'grapheme' });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; });\n }\n return splitGraphemes(value);\n};\nvar segmentWords = function (value, styles) {\n if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n var segmenter = new Intl.Segmenter(void 0, {\n granularity: 'word'\n });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; });\n }\n return breakWords(value, styles);\n};\nvar breakText = function (value, styles) {\n return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);\n};\n// https://drafts.csswg.org/css-text/#word-separator\nvar wordSeparators = [0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091];\nvar breakWords = function (str, styles) {\n var breaker = LineBreaker(str, {\n lineBreak: styles.lineBreak,\n wordBreak: styles.overflowWrap === \"break-word\" /* BREAK_WORD */ ? 'break-word' : styles.wordBreak\n });\n var words = [];\n var bk;\n var _loop_1 = function () {\n if (bk.value) {\n var value = bk.value.slice();\n var codePoints = toCodePoints$1(value);\n var word_1 = '';\n codePoints.forEach(function (codePoint) {\n if (wordSeparators.indexOf(codePoint) === -1) {\n word_1 += fromCodePoint$1(codePoint);\n }\n else {\n if (word_1.length) {\n words.push(word_1);\n }\n words.push(fromCodePoint$1(codePoint));\n word_1 = '';\n }\n });\n if (word_1.length) {\n words.push(word_1);\n }\n }\n };\n while (!(bk = breaker.next()).done) {\n _loop_1();\n }\n return words;\n};\n\nvar TextContainer = /** @class */ (function () {\n function TextContainer(context, node, styles) {\n this.text = transform(node.data, styles.textTransform);\n this.textBounds = parseTextBounds(context, this.text, styles, node);\n }\n return TextContainer;\n}());\nvar transform = function (text, transform) {\n switch (transform) {\n case 1 /* LOWERCASE */:\n return text.toLowerCase();\n case 3 /* CAPITALIZE */:\n return text.replace(CAPITALIZE, capitalize);\n case 2 /* UPPERCASE */:\n return text.toUpperCase();\n default:\n return text;\n }\n};\nvar CAPITALIZE = /(^|\\s|:|-|\\(|\\))([a-z])/g;\nvar capitalize = function (m, p1, p2) {\n if (m.length > 0) {\n return p1 + p2.toUpperCase();\n }\n return m;\n};\n\nvar ImageElementContainer = /** @class */ (function (_super) {\n __extends(ImageElementContainer, _super);\n function ImageElementContainer(context, img) {\n var _this = _super.call(this, context, img) || this;\n _this.src = img.currentSrc || img.src;\n _this.intrinsicWidth = img.naturalWidth;\n _this.intrinsicHeight = img.naturalHeight;\n _this.context.cache.addImage(_this.src);\n return _this;\n }\n return ImageElementContainer;\n}(ElementContainer));\n\nvar CanvasElementContainer = /** @class */ (function (_super) {\n __extends(CanvasElementContainer, _super);\n function CanvasElementContainer(context, canvas) {\n var _this = _super.call(this, context, canvas) || this;\n _this.canvas = canvas;\n _this.intrinsicWidth = canvas.width;\n _this.intrinsicHeight = canvas.height;\n return _this;\n }\n return CanvasElementContainer;\n}(ElementContainer));\n\nvar SVGElementContainer = /** @class */ (function (_super) {\n __extends(SVGElementContainer, _super);\n function SVGElementContainer(context, img) {\n var _this = _super.call(this, context, img) || this;\n var s = new XMLSerializer();\n var bounds = parseBounds(context, img);\n img.setAttribute('width', bounds.width + \"px\");\n img.setAttribute('height', bounds.height + \"px\");\n _this.svg = \"data:image/svg+xml,\" + encodeURIComponent(s.serializeToString(img));\n _this.intrinsicWidth = img.width.baseVal.value;\n _this.intrinsicHeight = img.height.baseVal.value;\n _this.context.cache.addImage(_this.svg);\n return _this;\n }\n return SVGElementContainer;\n}(ElementContainer));\n\nvar LIElementContainer = /** @class */ (function (_super) {\n __extends(LIElementContainer, _super);\n function LIElementContainer(context, element) {\n var _this = _super.call(this, context, element) || this;\n _this.value = element.value;\n return _this;\n }\n return LIElementContainer;\n}(ElementContainer));\n\nvar OLElementContainer = /** @class */ (function (_super) {\n __extends(OLElementContainer, _super);\n function OLElementContainer(context, element) {\n var _this = _super.call(this, context, element) || this;\n _this.start = element.start;\n _this.reversed = typeof element.reversed === 'boolean' && element.reversed === true;\n return _this;\n }\n return OLElementContainer;\n}(ElementContainer));\n\nvar CHECKBOX_BORDER_RADIUS = [\n {\n type: 15 /* DIMENSION_TOKEN */,\n flags: 0,\n unit: 'px',\n number: 3\n }\n];\nvar RADIO_BORDER_RADIUS = [\n {\n type: 16 /* PERCENTAGE_TOKEN */,\n flags: 0,\n number: 50\n }\n];\nvar reformatInputBounds = function (bounds) {\n if (bounds.width > bounds.height) {\n return new Bounds(bounds.left + (bounds.width - bounds.height) / 2, bounds.top, bounds.height, bounds.height);\n }\n else if (bounds.width < bounds.height) {\n return new Bounds(bounds.left, bounds.top + (bounds.height - bounds.width) / 2, bounds.width, bounds.width);\n }\n return bounds;\n};\nvar getInputValue = function (node) {\n var value = node.type === PASSWORD ? new Array(node.value.length + 1).join('\\u2022') : node.value;\n return value.length === 0 ? node.placeholder || '' : value;\n};\nvar CHECKBOX = 'checkbox';\nvar RADIO = 'radio';\nvar PASSWORD = 'password';\nvar INPUT_COLOR = 0x2a2a2aff;\nvar InputElementContainer = /** @class */ (function (_super) {\n __extends(InputElementContainer, _super);\n function InputElementContainer(context, input) {\n var _this = _super.call(this, context, input) || this;\n _this.type = input.type.toLowerCase();\n _this.checked = input.checked;\n _this.value = getInputValue(input);\n if (_this.type === CHECKBOX || _this.type === RADIO) {\n _this.styles.backgroundColor = 0xdededeff;\n _this.styles.borderTopColor =\n _this.styles.borderRightColor =\n _this.styles.borderBottomColor =\n _this.styles.borderLeftColor =\n 0xa5a5a5ff;\n _this.styles.borderTopWidth =\n _this.styles.borderRightWidth =\n _this.styles.borderBottomWidth =\n _this.styles.borderLeftWidth =\n 1;\n _this.styles.borderTopStyle =\n _this.styles.borderRightStyle =\n _this.styles.borderBottomStyle =\n _this.styles.borderLeftStyle =\n 1 /* SOLID */;\n _this.styles.backgroundClip = [0 /* BORDER_BOX */];\n _this.styles.backgroundOrigin = [0 /* BORDER_BOX */];\n _this.bounds = reformatInputBounds(_this.bounds);\n }\n switch (_this.type) {\n case CHECKBOX:\n _this.styles.borderTopRightRadius =\n _this.styles.borderTopLeftRadius =\n _this.styles.borderBottomRightRadius =\n _this.styles.borderBottomLeftRadius =\n CHECKBOX_BORDER_RADIUS;\n break;\n case RADIO:\n _this.styles.borderTopRightRadius =\n _this.styles.borderTopLeftRadius =\n _this.styles.borderBottomRightRadius =\n _this.styles.borderBottomLeftRadius =\n RADIO_BORDER_RADIUS;\n break;\n }\n return _this;\n }\n return InputElementContainer;\n}(ElementContainer));\n\nvar SelectElementContainer = /** @class */ (function (_super) {\n __extends(SelectElementContainer, _super);\n function SelectElementContainer(context, element) {\n var _this = _super.call(this, context, element) || this;\n var option = element.options[element.selectedIndex || 0];\n _this.value = option ? option.text || '' : '';\n return _this;\n }\n return SelectElementContainer;\n}(ElementContainer));\n\nvar TextareaElementContainer = /** @class */ (function (_super) {\n __extends(TextareaElementContainer, _super);\n function TextareaElementContainer(context, element) {\n var _this = _super.call(this, context, element) || this;\n _this.value = element.value;\n return _this;\n }\n return TextareaElementContainer;\n}(ElementContainer));\n\nvar IFrameElementContainer = /** @class */ (function (_super) {\n __extends(IFrameElementContainer, _super);\n function IFrameElementContainer(context, iframe) {\n var _this = _super.call(this, context, iframe) || this;\n _this.src = iframe.src;\n _this.width = parseInt(iframe.width, 10) || 0;\n _this.height = parseInt(iframe.height, 10) || 0;\n _this.backgroundColor = _this.styles.backgroundColor;\n try {\n if (iframe.contentWindow &&\n iframe.contentWindow.document &&\n iframe.contentWindow.document.documentElement) {\n _this.tree = parseTree(context, iframe.contentWindow.document.documentElement);\n // http://www.w3.org/TR/css3-background/#special-backgrounds\n var documentBackgroundColor = iframe.contentWindow.document.documentElement\n ? parseColor(context, getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor)\n : COLORS.TRANSPARENT;\n var bodyBackgroundColor = iframe.contentWindow.document.body\n ? parseColor(context, getComputedStyle(iframe.contentWindow.document.body).backgroundColor)\n : COLORS.TRANSPARENT;\n _this.backgroundColor = isTransparent(documentBackgroundColor)\n ? isTransparent(bodyBackgroundColor)\n ? _this.styles.backgroundColor\n : bodyBackgroundColor\n : documentBackgroundColor;\n }\n }\n catch (e) { }\n return _this;\n }\n return IFrameElementContainer;\n}(ElementContainer));\n\nvar LIST_OWNERS = ['OL', 'UL', 'MENU'];\nvar parseNodeTree = function (context, node, parent, root) {\n for (var childNode = node.firstChild, nextNode = void 0; childNode; childNode = nextNode) {\n nextNode = childNode.nextSibling;\n if (isTextNode(childNode) && childNode.data.trim().length > 0) {\n parent.textNodes.push(new TextContainer(context, childNode, parent.styles));\n }\n else if (isElementNode(childNode)) {\n if (isSlotElement(childNode) && childNode.assignedNodes) {\n childNode.assignedNodes().forEach(function (childNode) { return parseNodeTree(context, childNode, parent, root); });\n }\n else {\n var container = createContainer(context, childNode);\n if (container.styles.isVisible()) {\n if (createsRealStackingContext(childNode, container, root)) {\n container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */;\n }\n else if (createsStackingContext(container.styles)) {\n container.flags |= 2 /* CREATES_STACKING_CONTEXT */;\n }\n if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {\n container.flags |= 8 /* IS_LIST_OWNER */;\n }\n parent.elements.push(container);\n childNode.slot;\n if (childNode.shadowRoot) {\n parseNodeTree(context, childNode.shadowRoot, container, root);\n }\n else if (!isTextareaElement(childNode) &&\n !isSVGElement(childNode) &&\n !isSelectElement(childNode)) {\n parseNodeTree(context, childNode, container, root);\n }\n }\n }\n }\n }\n};\nvar createContainer = function (context, element) {\n if (isImageElement(element)) {\n return new ImageElementContainer(context, element);\n }\n if (isCanvasElement(element)) {\n return new CanvasElementContainer(context, element);\n }\n if (isSVGElement(element)) {\n return new SVGElementContainer(context, element);\n }\n if (isLIElement(element)) {\n return new LIElementContainer(context, element);\n }\n if (isOLElement(element)) {\n return new OLElementContainer(context, element);\n }\n if (isInputElement(element)) {\n return new InputElementContainer(context, element);\n }\n if (isSelectElement(element)) {\n return new SelectElementContainer(context, element);\n }\n if (isTextareaElement(element)) {\n return new TextareaElementContainer(context, element);\n }\n if (isIFrameElement(element)) {\n return new IFrameElementContainer(context, element);\n }\n return new ElementContainer(context, element);\n};\nvar parseTree = function (context, element) {\n var container = createContainer(context, element);\n container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */;\n parseNodeTree(context, element, container, container);\n return container;\n};\nvar createsRealStackingContext = function (node, container, root) {\n return (container.styles.isPositionedWithZIndex() ||\n container.styles.opacity < 1 ||\n container.styles.isTransformed() ||\n (isBodyElement(node) && root.styles.isTransparent()));\n};\nvar createsStackingContext = function (styles) { return styles.isPositioned() || styles.isFloating(); };\nvar isTextNode = function (node) { return node.nodeType === Node.TEXT_NODE; };\nvar isElementNode = function (node) { return node.nodeType === Node.ELEMENT_NODE; };\nvar isHTMLElementNode = function (node) {\n return isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);\n};\nvar isSVGElementNode = function (element) {\n return typeof element.className === 'object';\n};\nvar isLIElement = function (node) { return node.tagName === 'LI'; };\nvar isOLElement = function (node) { return node.tagName === 'OL'; };\nvar isInputElement = function (node) { return node.tagName === 'INPUT'; };\nvar isHTMLElement = function (node) { return node.tagName === 'HTML'; };\nvar isSVGElement = function (node) { return node.tagName === 'svg'; };\nvar isBodyElement = function (node) { return node.tagName === 'BODY'; };\nvar isCanvasElement = function (node) { return node.tagName === 'CANVAS'; };\nvar isVideoElement = function (node) { return node.tagName === 'VIDEO'; };\nvar isImageElement = function (node) { return node.tagName === 'IMG'; };\nvar isIFrameElement = function (node) { return node.tagName === 'IFRAME'; };\nvar isStyleElement = function (node) { return node.tagName === 'STYLE'; };\nvar isScriptElement = function (node) { return node.tagName === 'SCRIPT'; };\nvar isTextareaElement = function (node) { return node.tagName === 'TEXTAREA'; };\nvar isSelectElement = function (node) { return node.tagName === 'SELECT'; };\nvar isSlotElement = function (node) { return node.tagName === 'SLOT'; };\n// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name\nvar isCustomElement = function (node) { return node.tagName.indexOf('-') > 0; };\n\nvar CounterState = /** @class */ (function () {\n function CounterState() {\n this.counters = {};\n }\n CounterState.prototype.getCounterValue = function (name) {\n var counter = this.counters[name];\n if (counter && counter.length) {\n return counter[counter.length - 1];\n }\n return 1;\n };\n CounterState.prototype.getCounterValues = function (name) {\n var counter = this.counters[name];\n return counter ? counter : [];\n };\n CounterState.prototype.pop = function (counters) {\n var _this = this;\n counters.forEach(function (counter) { return _this.counters[counter].pop(); });\n };\n CounterState.prototype.parse = function (style) {\n var _this = this;\n var counterIncrement = style.counterIncrement;\n var counterReset = style.counterReset;\n var canReset = true;\n if (counterIncrement !== null) {\n counterIncrement.forEach(function (entry) {\n var counter = _this.counters[entry.counter];\n if (counter && entry.increment !== 0) {\n canReset = false;\n if (!counter.length) {\n counter.push(1);\n }\n counter[Math.max(0, counter.length - 1)] += entry.increment;\n }\n });\n }\n var counterNames = [];\n if (canReset) {\n counterReset.forEach(function (entry) {\n var counter = _this.counters[entry.counter];\n counterNames.push(entry.counter);\n if (!counter) {\n counter = _this.counters[entry.counter] = [];\n }\n counter.push(entry.reset);\n });\n }\n return counterNames;\n };\n return CounterState;\n}());\nvar ROMAN_UPPER = {\n integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],\n values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']\n};\nvar ARMENIAN = {\n integers: [\n 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70,\n 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1\n ],\n values: [\n 'Ք',\n 'Փ',\n 'Ւ',\n 'Ց',\n 'Ր',\n 'Տ',\n 'Վ',\n 'Ս',\n 'Ռ',\n 'Ջ',\n 'Պ',\n 'Չ',\n 'Ո',\n 'Շ',\n 'Ն',\n 'Յ',\n 'Մ',\n 'Ճ',\n 'Ղ',\n 'Ձ',\n 'Հ',\n 'Կ',\n 'Ծ',\n 'Խ',\n 'Լ',\n 'Ի',\n 'Ժ',\n 'Թ',\n 'Ը',\n 'Է',\n 'Զ',\n 'Ե',\n 'Դ',\n 'Գ',\n 'Բ',\n 'Ա'\n ]\n};\nvar HEBREW = {\n integers: [\n 10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20,\n 19, 18, 17, 16, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1\n ],\n values: [\n 'י׳',\n 'ט׳',\n 'ח׳',\n 'ז׳',\n 'ו׳',\n 'ה׳',\n 'ד׳',\n 'ג׳',\n 'ב׳',\n 'א׳',\n 'ת',\n 'ש',\n 'ר',\n 'ק',\n 'צ',\n 'פ',\n 'ע',\n 'ס',\n 'נ',\n 'מ',\n 'ל',\n 'כ',\n 'יט',\n 'יח',\n 'יז',\n 'טז',\n 'טו',\n 'י',\n 'ט',\n 'ח',\n 'ז',\n 'ו',\n 'ה',\n 'ד',\n 'ג',\n 'ב',\n 'א'\n ]\n};\nvar GEORGIAN = {\n integers: [\n 10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90,\n 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1\n ],\n values: [\n 'ჵ',\n 'ჰ',\n 'ჯ',\n 'ჴ',\n 'ხ',\n 'ჭ',\n 'წ',\n 'ძ',\n 'ც',\n 'ჩ',\n 'შ',\n 'ყ',\n 'ღ',\n 'ქ',\n 'ფ',\n 'ჳ',\n 'ტ',\n 'ს',\n 'რ',\n 'ჟ',\n 'პ',\n 'ო',\n 'ჲ',\n 'ნ',\n 'მ',\n 'ლ',\n 'კ',\n 'ი',\n 'თ',\n 'ჱ',\n 'ზ',\n 'ვ',\n 'ე',\n 'დ',\n 'გ',\n 'ბ',\n 'ა'\n ]\n};\nvar createAdditiveCounter = function (value, min, max, symbols, fallback, suffix) {\n if (value < min || value > max) {\n return createCounterText(value, fallback, suffix.length > 0);\n }\n return (symbols.integers.reduce(function (string, integer, index) {\n while (value >= integer) {\n value -= integer;\n string += symbols.values[index];\n }\n return string;\n }, '') + suffix);\n};\nvar createCounterStyleWithSymbolResolver = function (value, codePointRangeLength, isNumeric, resolver) {\n var string = '';\n do {\n if (!isNumeric) {\n value--;\n }\n string = resolver(value) + string;\n value /= codePointRangeLength;\n } while (value * codePointRangeLength >= codePointRangeLength);\n return string;\n};\nvar createCounterStyleFromRange = function (value, codePointRangeStart, codePointRangeEnd, isNumeric, suffix) {\n var codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;\n return ((value < 0 ? '-' : '') +\n (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, isNumeric, function (codePoint) {\n return fromCodePoint$1(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart);\n }) +\n suffix));\n};\nvar createCounterStyleFromSymbols = function (value, symbols, suffix) {\n if (suffix === void 0) { suffix = '. '; }\n var codePointRangeLength = symbols.length;\n return (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, false, function (codePoint) { return symbols[Math.floor(codePoint % codePointRangeLength)]; }) + suffix);\n};\nvar CJK_ZEROS = 1 << 0;\nvar CJK_TEN_COEFFICIENTS = 1 << 1;\nvar CJK_TEN_HIGH_COEFFICIENTS = 1 << 2;\nvar CJK_HUNDRED_COEFFICIENTS = 1 << 3;\nvar createCJKCounter = function (value, numbers, multipliers, negativeSign, suffix, flags) {\n if (value < -9999 || value > 9999) {\n return createCounterText(value, 4 /* CJK_DECIMAL */, suffix.length > 0);\n }\n var tmp = Math.abs(value);\n var string = suffix;\n if (tmp === 0) {\n return numbers[0] + string;\n }\n for (var digit = 0; tmp > 0 && digit <= 4; digit++) {\n var coefficient = tmp % 10;\n if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') {\n string = numbers[coefficient] + string;\n }\n else if (coefficient > 1 ||\n (coefficient === 1 && digit === 0) ||\n (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) ||\n (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_HIGH_COEFFICIENTS) && value > 100) ||\n (coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))) {\n string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string;\n }\n else if (coefficient === 1 && digit > 0) {\n string = multipliers[digit - 1] + string;\n }\n tmp = Math.floor(tmp / 10);\n }\n return (value < 0 ? negativeSign : '') + string;\n};\nvar CHINESE_INFORMAL_MULTIPLIERS = '十百千萬';\nvar CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';\nvar JAPANESE_NEGATIVE = 'マイナス';\nvar KOREAN_NEGATIVE = '마이너스';\nvar createCounterText = function (value, type, appendSuffix) {\n var defaultSuffix = appendSuffix ? '. ' : '';\n var cjkSuffix = appendSuffix ? '、' : '';\n var koreanSuffix = appendSuffix ? ', ' : '';\n var spaceSuffix = appendSuffix ? ' ' : '';\n switch (type) {\n case 0 /* DISC */:\n return '•' + spaceSuffix;\n case 1 /* CIRCLE */:\n return '◦' + spaceSuffix;\n case 2 /* SQUARE */:\n return '◾' + spaceSuffix;\n case 5 /* DECIMAL_LEADING_ZERO */:\n var string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);\n return string.length < 4 ? \"0\" + string : string;\n case 4 /* CJK_DECIMAL */:\n return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);\n case 6 /* LOWER_ROMAN */:\n return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix).toLowerCase();\n case 7 /* UPPER_ROMAN */:\n return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix);\n case 8 /* LOWER_GREEK */:\n return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);\n case 9 /* LOWER_ALPHA */:\n return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);\n case 10 /* UPPER_ALPHA */:\n return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);\n case 11 /* ARABIC_INDIC */:\n return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);\n case 12 /* ARMENIAN */:\n case 49 /* UPPER_ARMENIAN */:\n return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix);\n case 35 /* LOWER_ARMENIAN */:\n return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix).toLowerCase();\n case 13 /* BENGALI */:\n return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);\n case 14 /* CAMBODIAN */:\n case 30 /* KHMER */:\n return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);\n case 15 /* CJK_EARTHLY_BRANCH */:\n return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);\n case 16 /* CJK_HEAVENLY_STEM */:\n return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);\n case 17 /* CJK_IDEOGRAPHIC */:\n case 48 /* TRAD_CHINESE_INFORMAL */:\n return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);\n case 47 /* TRAD_CHINESE_FORMAL */:\n return createCJKCounter(value, '零壹貳參肆伍陸柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);\n case 42 /* SIMP_CHINESE_INFORMAL */:\n return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);\n case 41 /* SIMP_CHINESE_FORMAL */:\n return createCJKCounter(value, '零壹贰叁肆伍陆柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);\n case 26 /* JAPANESE_INFORMAL */:\n return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);\n case 25 /* JAPANESE_FORMAL */:\n return createCJKCounter(value, '零壱弐参四伍六七八九', '拾百千万', JAPANESE_NEGATIVE, cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);\n case 31 /* KOREAN_HANGUL_FORMAL */:\n return createCJKCounter(value, '영일이삼사오육칠팔구', '십백천만', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);\n case 33 /* KOREAN_HANJA_INFORMAL */:\n return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);\n case 32 /* KOREAN_HANJA_FORMAL */:\n return createCJKCounter(value, '零壹貳參四五六七八九', '拾百千', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);\n case 18 /* DEVANAGARI */:\n return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);\n case 20 /* GEORGIAN */:\n return createAdditiveCounter(value, 1, 19999, GEORGIAN, 3 /* DECIMAL */, defaultSuffix);\n case 21 /* GUJARATI */:\n return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);\n case 22 /* GURMUKHI */:\n return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);\n case 22 /* HEBREW */:\n return createAdditiveCounter(value, 1, 10999, HEBREW, 3 /* DECIMAL */, defaultSuffix);\n case 23 /* HIRAGANA */:\n return createCounterStyleFromSymbols(value, 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん');\n case 24 /* HIRAGANA_IROHA */:\n return createCounterStyleFromSymbols(value, 'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす');\n case 27 /* KANNADA */:\n return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);\n case 28 /* KATAKANA */:\n return createCounterStyleFromSymbols(value, 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン', cjkSuffix);\n case 29 /* KATAKANA_IROHA */:\n return createCounterStyleFromSymbols(value, 'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス', cjkSuffix);\n case 34 /* LAO */:\n return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);\n case 37 /* MONGOLIAN */:\n return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);\n case 38 /* MYANMAR */:\n return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);\n case 39 /* ORIYA */:\n return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);\n case 40 /* PERSIAN */:\n return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);\n case 43 /* TAMIL */:\n return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);\n case 44 /* TELUGU */:\n return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);\n case 45 /* THAI */:\n return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);\n case 46 /* TIBETAN */:\n return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);\n case 3 /* DECIMAL */:\n default:\n return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);\n }\n};\n\nvar IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';\nvar DocumentCloner = /** @class */ (function () {\n function DocumentCloner(context, element, options) {\n this.context = context;\n this.options = options;\n this.scrolledElements = [];\n this.referenceElement = element;\n this.counters = new CounterState();\n this.quoteDepth = 0;\n if (!element.ownerDocument) {\n throw new Error('Cloned element does not have an owner document');\n }\n this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);\n }\n DocumentCloner.prototype.toIFrame = function (ownerDocument, windowSize) {\n var _this = this;\n var iframe = createIFrameContainer(ownerDocument, windowSize);\n if (!iframe.contentWindow) {\n return Promise.reject(\"Unable to find iframe window\");\n }\n var scrollX = ownerDocument.defaultView.pageXOffset;\n var scrollY = ownerDocument.defaultView.pageYOffset;\n var cloneWindow = iframe.contentWindow;\n var documentClone = cloneWindow.document;\n /* Chrome doesn't detect relative background-images assigned in inline LUPMIS2 Drawing Tools @@ -106,6 +151,295 @@ --radius-2xl: 1rem; } + /* ─── Fieldwork Mode ─── high-contrast + larger touch targets ─── */ + .fieldwork-mode { + --foreground: #000; + --background: #fff; + --card: #fff; + --card-foreground: #000; + --primary: #0044aa; + --primary-foreground: #fff; + --primary-hover: #003080; + --muted: #e0e0e0; + --muted-foreground: #333; + --accent: #cce0ff; + --accent-foreground: #000; + --border: rgba(0,0,0,0.25); + --success: #005a00; + --success-foreground: #fff; + --warning: #b36b00; + --warning-foreground: #000; + --destructive: #b80000; + --destructive-foreground: #fff; + --ring: #0044aa; + --bs-body-color: #000; + } + + /* Fieldwork: larger dock buttons */ + .fieldwork-mode .dock-btn { + min-width: 72px; + min-height: 58px; + font-size: 1.6rem; + border-width: 2px; + } + .fieldwork-mode .dock-btn-label { + font-size: 0.75rem; + font-weight: 600; + } + + /* Fieldwork: bolder navbar */ + .fieldwork-mode .navbar { + border-bottom-width: 4px; + } + .fieldwork-mode .navbar .navbar-brand { + font-size: 1.6rem; + } + + /* Fieldwork: larger offcanvas toggle buttons */ + .fieldwork-mode .offcanvas-toggle { + width: 44px; + height: 44px; + font-size: 1.2rem; + } + + /* Fieldwork: thicker bottom dock border */ + .fieldwork-mode .bottom-dock { + border-top-width: 4px; + } + + /* Fieldwork: larger text in cards / lists */ + .fieldwork-mode .card-header h6 { + font-size: 1rem; + } + .fieldwork-mode .list-group-item { + font-size: 0.95rem; + padding: 0.65rem 1rem; + } + + /* Fieldwork: larger buttons globally */ + .fieldwork-mode .btn { + font-size: 0.95rem; + padding: 0.5rem 1rem; + font-weight: 600; + } + .fieldwork-mode .btn-sm { + font-size: 0.85rem; + padding: 0.4rem 0.75rem; + } + + /* Fieldwork: stronger borders on inputs / form controls */ + .fieldwork-mode .form-control, + .fieldwork-mode .form-select { + border-width: 2px; + border-color: #555; + font-size: 1rem; + } + + /* Fieldwork: bolder map controls (ol-ext) */ + .fieldwork-mode .ol-control button { + font-size: 1.3rem; + width: 2.2em; + height: 2.2em; + } + + /* Fieldwork: scale bar text legibility */ + .fieldwork-mode .ol-scale-bar .ol-scale-step-text, + .fieldwork-mode .ol-scale-bar .ol-scale-text { + font-size: 12px; + font-weight: 700; + text-shadow: 0 0 4px #fff, 0 0 8px #fff; + } + + /* ─── Dark Mode ─── reversed colour scheme ─── */ + .dark-mode { + --foreground: #e0dff0; + --background: #131325; + --card: #1e1e38; + --card-foreground: #e0dff0; + --primary: #4d9de6; + --primary-foreground: #fff; + --primary-hover: #6fb3f0; + --muted: #272745; + --muted-foreground: #9594a8; + --accent: #1e3a5f; + --accent-foreground: #e0dff0; + --border: rgba(255,255,255,0.12); + --ring: #4d9de6; + --success: #2dd46a; + --success-foreground: #131325; + --warning: #ffb84d; + --warning-foreground: #131325; + --destructive: #f04040; + --destructive-foreground: #fff; + --bs-body-color: #e0dff0; + --bs-body-bg: #131325; + --bs-tertiary-bg: #1e1e38; + color-scheme: dark; + } + + /* Dark: navbar */ + .dark-mode .navbar { + background-color: #1a1a30 !important; + box-shadow: 0 1px 6px rgba(0,0,0,0.4); + } + + /* Dark: bottom dock */ + .dark-mode .bottom-dock { + background-color: #1a1a30; + box-shadow: 0 -2px 10px rgba(0,0,0,0.3); + } + .dark-mode .dock-btn { + border-color: var(--primary); + color: var(--foreground); + } + .dark-mode .dock-btn:hover { + background-color: var(--muted); + } + .dark-mode .dock-btn.active { + background-color: var(--primary); + color: var(--primary-foreground); + } + + /* Dark: offcanvas panels */ + .dark-mode .offcanvas { + background-color: var(--background) !important; + color: var(--foreground) !important; + } + .dark-mode .offcanvas-header { + border-bottom-color: var(--border) !important; + } + .dark-mode .btn-close { + filter: invert(1) grayscale(100%) brightness(200%); + } + + /* Dark: cards */ + .dark-mode .card { + background-color: var(--card) !important; + color: var(--card-foreground) !important; + border-color: var(--border) !important; + } + + /* Dark: offcanvas toggle buttons */ + .dark-mode .offcanvas-toggle { + background-color: var(--card); + color: var(--foreground); + border-color: var(--border); + } + .dark-mode .offcanvas-toggle:hover { + background-color: var(--primary); + color: var(--primary-foreground); + } + + /* Dark: form controls */ + .dark-mode .form-control, + .dark-mode .form-select { + background-color: var(--muted) !important; + color: var(--foreground) !important; + border-color: var(--border) !important; + } + .dark-mode .form-check-input { + background-color: var(--muted); + border-color: var(--muted-foreground); + } + .dark-mode .form-check-input:checked { + background-color: var(--primary); + border-color: var(--primary); + } + + /* Dark: list groups */ + .dark-mode .list-group-item { + background-color: var(--card) !important; + color: var(--card-foreground) !important; + border-color: var(--border) !important; + } + + /* Dark: buttons */ + .dark-mode .btn-outline-primary { + color: var(--primary); + border-color: var(--primary); + } + .dark-mode .btn-outline-danger { + color: var(--destructive); + border-color: var(--destructive); + } + + /* Dark: text utilities */ + .dark-mode .text-muted { + color: var(--muted-foreground) !important; + } + + /* Dark: measurement tooltips */ + .dark-mode .measure-tooltip { + background: rgba(30, 30, 56, 0.95); + color: var(--foreground); + border-color: var(--primary); + } + .dark-mode .measure-tooltip::before { + border-right-color: var(--primary); + } + + /* Dark: OL controls */ + .dark-mode .ol-control button { + background-color: var(--card) !important; + color: var(--foreground) !important; + } + .dark-mode .ol-control button:hover { + background-color: var(--primary) !important; + color: var(--primary-foreground) !important; + } + .dark-mode .ol-attribution, + .dark-mode .ol-attribution a { + color: var(--muted-foreground) !important; + } + + /* Dark: scale bar */ + .dark-mode .ol-scale-bar .ol-scale-step-text, + .dark-mode .ol-scale-bar .ol-scale-text { + color: #fff !important; + text-shadow: 0 0 4px #000, 0 0 8px #000 !important; + } + .dark-mode .ol-scale-bar .ol-scale-singlebar-even { + background-color: #fff !important; + } + .dark-mode .ol-scale-bar .ol-scale-singlebar-odd { + background-color: #999 !important; + } + + /* Dark: map drop overlay */ + .dark-mode .map-drop-overlay { + background: rgba(19, 19, 37, 0.85); + border-color: var(--primary); + color: var(--foreground); + } + + /* Dark: ol-ext LayerSwitcher */ + .dark-mode .ol-layerswitcher { + background-color: var(--card) !important; + } + .dark-mode .ol-layerswitcher .panel { + background-color: var(--card) !important; + color: var(--foreground) !important; + } + .dark-mode .ol-layerswitcher .panel li { + color: var(--foreground); + } + .dark-mode .ol-layerswitcher .ol-switchertopdiv, + .dark-mode .ol-layerswitcher .ol-switcherbottomdiv { + background: var(--card) !important; + } + + /* Dark: alert boxes */ + .dark-mode .alert-danger { + background-color: rgba(240, 64, 64, 0.15) !important; + color: var(--destructive) !important; + border-color: var(--destructive) !important; + } + .dark-mode .alert-success { + background-color: rgba(45, 212, 106, 0.15) !important; + color: var(--success) !important; + border-color: var(--success) !important; + } + /* Full height layout */ html, body { height: 100%; @@ -214,9 +548,12 @@ font-family: var(--font-body); } - /* Main container - full height */ + /* Main container - full height. + 100dvh accounts for mobile browser chrome and OS nav bars. + Falls back to 100vh for older browsers. */ .app-container { height: 100vh; + height: 100dvh; display: flex; flex-direction: column; } @@ -244,6 +581,35 @@ height: 100%; } + /* Drag-and-drop overlay shown when files are dragged over the map */ + .map-drop-overlay { + position: absolute; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 94, 184, 0.15); + border: 3px dashed var(--primary, #005eb8); + border-radius: 8px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + } + .map-container.drag-over .map-drop-overlay { + opacity: 1; + } + .map-drop-overlay span { + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 1.15rem; + font-weight: 600; + color: var(--primary, #005eb8); + background: var(--card, #fff); + padding: 0.6rem 1.4rem; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.12); + } + /* Offline indicator */ #offline-indicator { display: none; @@ -301,9 +667,14 @@ z-index: 1050; } - /* Fix ol-ext LayerSwitcher z-index */ - .ol-layerswitcher { - z-index: 100; + /* OL controls stacking context fix — OpenLayers sets z-index:0 on + .ol-overlaycontainer-stopevent, trapping all controls below the + offcanvas-toggle buttons (z-index:500). Raising the container + to 501 lets the LayerSwitcher dropdown render above the toggles. + pointer-events:none on the container still lets clicks through + to the toggle buttons underneath. */ + .ol-overlaycontainer-stopevent { + z-index: 501 !important; } /* Alert hint box */ @@ -419,7 +790,7 @@ } .offcanvas-toggle-bottom { - bottom: 80px; /* Above the dock */ + bottom: calc(80px + env(safe-area-inset-bottom, 0px)); /* Above the dock */ left: 50%; transform: translateX(-50%); } @@ -432,7 +803,10 @@ transform: translateX(-50%) scale(0.95); } - /* Bottom Dock — white card style with blue-strong accent */ + /* Bottom Dock — white card style with blue-strong accent. + env(safe-area-inset-bottom) adds padding on devices with a + home indicator / gesture bar (e.g. iPhone notch models). + The value is 0 on devices without an inset. */ .bottom-dock { position: absolute; bottom: 0; @@ -441,7 +815,7 @@ z-index: 600; background-color: var(--card); border-top: 3px solid var(--primary); - padding: 8px 16px; + padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); display: flex; justify-content: space-around; align-items: center; @@ -503,6 +877,13 @@ justify-content: center; } + /* Snap-guides toggle — highlighted when active */ + .ol-snap-toggle.ol-active button { + background: var(--primary) !important; + color: var(--primary-foreground, #fff) !important; + border-radius: 3px; + } + /* Touch-friendly improvements for forms and buttons */ .form-control, .form-select { min-height: 44px; @@ -541,6 +922,18 @@ color: var(--foreground); } + /* Message log in the right panel */ + .message-log { + max-height: 260px; + overflow-y: auto; + } + + .message-log-entry { + font-size: 0.82rem; + border-color: var(--border, #eee) !important; + background: transparent; + } + /* ol-ext GeolocationButton styling */ .ol-geobt { top: auto !important; @@ -830,6 +1223,7 @@ /* Locations list in offcanvas - can be taller now without form */ .offcanvas-end .locations-list { max-height: calc(100vh - 280px); + max-height: calc(100dvh - 280px); overflow-y: auto; } @@ -921,17 +1315,32 @@ font-size: 18px; } - /* ScaleLine - position above the bottom dock */ - .ol-scale-line { - bottom: 76px !important; + /* ScaleBar - position above the bottom dock with 4px gap */ + .ol-scale-bar { + bottom: calc(85px + env(safe-area-inset-bottom, 0px)) !important; left: 10px !important; } - .ol-scale-line-inner { - border-color: var(--foreground) !important; + .ol-scale-bar .ol-scale-step-text { color: var(--foreground) !important; font-family: var(--font-body) !important; font-size: 11px !important; + text-shadow: 0 0 3px var(--background), 0 0 6px var(--background) !important; + } + + .ol-scale-bar .ol-scale-text { + color: var(--foreground) !important; + font-family: var(--font-body) !important; + font-size: 11px !important; + text-shadow: 0 0 3px var(--background), 0 0 6px var(--background) !important; + } + + .ol-scale-bar .ol-scale-singlebar-even { + background-color: var(--foreground) !important; + } + + .ol-scale-bar .ol-scale-singlebar-odd { + background-color: var(--muted-foreground) !important; } /* ol-ext Bar overrides */ @@ -950,12 +1359,12 @@ gap: 2px; } - + + - - - + + @@ -988,6 +1397,7 @@
+
Drop file to import (.shp .geojson .kml)
+ + + + + + + +
-
+
Local Database Tables
+
- + + @@ -1150,6 +1583,10 @@ + + + +
+
+
Messages
+ +
+
+
+
+ No messages yet. +
+
+
+
+
-
Bottom Panel
+
Settings
-
+
-

This is the bottom offcanvas panel.

-

You can add a data table, charts, or other wide content here.

+
+ +
+
+
+
+
+
Fieldwork Mode
+ High-contrast colours and larger touch targets for bright sunlight and field conditions. +
+
+ +
+
+
+ +
+
+
+
+
+
Dark Mode
+ Reduce glare and save battery with a dark colour scheme. +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Measurement System
+ Switch between Metric (m, km) and Imperial (ft, mi, acres) units. +
+
+ + +
+
+
+
+
+ +
+
+
+
+
+
Default Base Map
+ Base map shown on app start. Saved on this device. +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Offline Map Tiles +
+ + Map tiles you've already viewed are cached on this device so they work offline. + Tiles are cached automatically as you browse, or you can pre-download a region. + +
+
+ + +
+
+
+
Loading…
+
+
+
+
+
+
+ + + + diff --git a/dist/manifest.json b/dist/manifest.json index d8ab69a..7ae399c 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -1,58 +1,58 @@ { "name": "LUPMIS2 Drawing Tools", - "short_name": "LUPMIS", + "short_name": "LUPMIS2", "description": "Map and GIS functions for Land Use Planning in Ghana", - "start_url": "/", - "scope": "/", + "start_url": "./", + "scope": "./", "display": "standalone", "background_color": "#ffffff", "theme_color": "#005eb8", "orientation": "any", "icons": [ { - "src": "/icons/icon-72.png", + "src": "./icons/luspa-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-96.png", + "src": "./icons/luspa-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-128.png", + "src": "./icons/luspa-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-144.png", + "src": "./icons/luspa-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-152.png", + "src": "./icons/luspa-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "any" }, { - "src": "/icons/luspa-192x192.png", + "src": "./icons/luspa-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icons/icon-384.png", + "src": "./icons/luspa-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "any" }, { - "src": "/icons/luspa-512x512.png", + "src": "./icons/luspa-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" diff --git a/dist/sw.js b/dist/sw.js index 257c9a9..4c981a5 100644 --- a/dist/sw.js +++ b/dist/sw.js @@ -1,29 +1,81 @@ /** * Service Worker - * + * * Handles caching of: * - App shell (HTML, CSS, JS) - * - Map tiles (runtime caching) + * - Map tiles (passive runtime caching, per-host buckets) * - API responses (network-first) - * + * * Note: Database operations are handled by the SharedWorker (shared-db-worker.js), * NOT by this service worker. They serve different purposes: * - Service Worker: Caching, offline asset serving, push notifications * - SharedWorker: Shared database connection across tabs */ -const CACHE_VERSION = 'v1'; -const SHELL_CACHE = `shell-${CACHE_VERSION}`; -const TILES_CACHE = `tiles-${CACHE_VERSION}`; +// v3: lower per-cache limits (5000 → 1500) and counter-based eviction to +// prevent Safari memory-pressure reloads. +// v4: raise OSM and Topographic limits to 8000 to support active offline +// downloads (Phase 2). Other providers stay at 1500. +const CACHE_VERSION = 'v4'; +const SHELL_CACHE = `shell-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`; -const API_CACHE = `api-${CACHE_VERSION}`; +const API_CACHE = `api-${CACHE_VERSION}`; -// Maximum number of tiles to cache -const MAX_TILES = 500; +// ---------------------------------------------------------------------------- +// Tile caches — one per provider so users can clear them independently. +// Limits are per-cache (not global). 5 000 tiles ≈ ~150 MB at ~30 KB/tile, +// which covers a Ghana district at zoom 10–15 (typical field-work range). +// ---------------------------------------------------------------------------- +const TILES_OSM = `tiles-osm-${CACHE_VERSION}`; +const TILES_TOPO = `tiles-topo-${CACHE_VERSION}`; +const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`; +const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`; +const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`; -// App shell assets - precached on install -// Vite will generate hashed filenames, so we cache the entry points -// and let the browser handle the hashed assets +// Per-provider tile limits. +// • OSM and Topographic are the providers offered for active offline +// download (Phase 2 dialog), so they get a higher cap (~240 MB each at +// ~30 KB/tile) — enough for a typical Ghana district at zoom 10–15. +// • The other providers serve passive caching only (whatever the user has +// already viewed), so 1 500 tiles ≈ 45 MB is plenty. +// +// Total max ≈ 5 × ~150 MB = ~750 MB on disk in the worst case, but only the +// two downloadable buckets are likely to fill. Eviction sweeps run every 100 +// inserts (see EVICTION_CHECK_INTERVAL) so memory pressure stays bounded. +const TILE_LIMITS = { + [TILES_OSM]: 8000, + [TILES_TOPO]: 8000, + [TILES_SATELLITE]: 1500, + [TILES_CARTO_LIGHT]: 1500, + [TILES_CARTO_DARK]: 1500, +}; + +// Per-cache running insert counter, in memory. Avoids calling cache.keys() +// (which materialises every Request object in the cache) on every put — that +// was the cause of the Safari "reloaded due to memory pressure" failures. +// +// We only run a real eviction sweep every EVICTION_CHECK_INTERVAL inserts. +const _tileInsertCounters = new Map(); // cacheName → number of inserts since last eviction +const EVICTION_CHECK_INTERVAL = 100; + +// Friendly name shown in the UI (matches Settings card labels) +const TILE_CACHE_LABELS = { + [TILES_OSM]: 'OpenStreetMap', + [TILES_TOPO]: 'Topographic', + [TILES_SATELLITE]: 'Satellite', + [TILES_CARTO_LIGHT]: 'Carto Light', + [TILES_CARTO_DARK]: 'Carto Dark', +}; + +const ALL_TILE_CACHES = Object.keys(TILE_LIMITS); + +// Approximate average tile size — used for storage estimation. +// Real measurements: PNG tiles range 5–80 KB; 30 KB is a good middle ground. +const AVG_TILE_BYTES = 30 * 1024; + +// ---------------------------------------------------------------------------- +// App shell assets — precached on install. +// ---------------------------------------------------------------------------- const SHELL_ASSETS = [ '/', '/index.html', @@ -37,7 +89,7 @@ const SHELL_ASSETS = [ self.addEventListener('install', (event) => { console.log('[SW] Installing...'); - + event.waitUntil( caches.open(SHELL_CACHE) .then((cache) => { @@ -54,18 +106,26 @@ self.addEventListener('install', (event) => { self.addEventListener('activate', (event) => { console.log('[SW] Activating...'); - + event.waitUntil( caches.keys() .then((cacheNames) => { + // Build the set of caches that should remain + const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]); + return Promise.all( cacheNames + // Delete anything that: + // • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-) + // • but is NOT in the current keep set + // This includes the legacy "tiles-v1" single bucket. .filter((name) => { - // Delete old version caches - return (name.startsWith('shell-') && name !== SHELL_CACHE) || - (name.startsWith('tiles-') && name !== TILES_CACHE) || - (name.startsWith('modules-') && name !== MODULES_CACHE) || - (name.startsWith('api-') && name !== API_CACHE); + const isOurs = + name.startsWith('shell-') || + name.startsWith('tiles-') || + name.startsWith('modules-') || + name.startsWith('api-'); + return isOurs && !keep.has(name); }) .map((name) => { console.log('[SW] Deleting old cache:', name); @@ -84,17 +144,28 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); - + // Only handle GET requests if (request.method !== 'GET') return; - + // Skip chrome-extension and other non-http(s) requests if (!url.protocol.startsWith('http')) return; - - // Route to appropriate caching strategy - if (isMapTile(url)) { - event.respondWith(cacheThenNetwork(request, TILES_CACHE, MAX_TILES)); - } else if (isApiRequest(url)) { + + // Skip worker files and Vite dev-server node_modules requests — + // intercepting these breaks module workers (e.g. SQLocal/SQLite). + if (url.pathname.includes('node_modules') || + url.search.includes('worker_file') || + request.destination === 'worker') return; + + // ----- TILE REQUESTS — passive cache-then-network (per-host bucket) ----- + const tileCache = getTileCacheName(url); + if (tileCache) { + event.respondWith(tileCacheThenNetwork(request, tileCache)); + return; + } + + // ----- OTHER ROUTES (unchanged) ----- + if (isApiRequest(url)) { event.respondWith(networkFirst(request, API_CACHE)); } else if (isModuleAsset(url)) { event.respondWith(staleWhileRevalidate(request, MODULES_CACHE)); @@ -108,15 +179,36 @@ self.addEventListener('fetch', (event) => { // URL CLASSIFICATION // ============================================================================ -function isMapTile(url) { - // Common tile server patterns for all our base maps - return url.hostname.includes('tile.openstreetmap.org') || - url.hostname.includes('opentopomap.org') || - url.hostname.includes('arcgisonline.com') || - url.hostname.includes('basemaps.cartocdn.com') || - url.hostname.includes('tiles.') || - url.pathname.match(/\/\d+\/\d+\/\d+\.(png|jpg|pbf)$/) || - url.pathname.match(/\/tile\/\d+\/\d+\/\d+/); +/** + * Classify a URL into the appropriate tile cache. + * Returns `null` for non-tile requests, or for tile providers we deliberately + * do NOT cache (e.g. Google — caching is forbidden by their ToS). + */ +function getTileCacheName(url) { + const host = url.hostname; + + // OpenStreetMap — tile.openstreetmap.org and a/b/c subdomains + if (host.endsWith('tile.openstreetmap.org')) return TILES_OSM; + + // OpenTopoMap — a/b/c.tile.opentopomap.org + if (host.endsWith('tile.opentopomap.org') || host.endsWith('opentopomap.org')) return TILES_TOPO; + + // Carto Basemaps — light_all / dark_all distinguished by path + if (host.endsWith('basemaps.cartocdn.com')) { + if (url.pathname.includes('/light_all/')) return TILES_CARTO_LIGHT; + if (url.pathname.includes('/dark_all/')) return TILES_CARTO_DARK; + return null; // unknown Carto style + } + + // Esri — server.arcgisonline.com + if (host.endsWith('arcgisonline.com')) return TILES_SATELLITE; + + // Google — caching forbidden by ToS, do not store + if (host.endsWith('google.com') || host.endsWith('googleapis.com')) return null; + + // Other tile providers (WMS endpoints, OWS, custom) — not cached at this layer + // (the user's "online only" toast handles those). + return null; } function isApiRequest(url) { @@ -129,7 +221,6 @@ function isModuleAsset(url) { } function isAppAsset(url) { - // Same origin, common asset extensions return url.origin === self.location.origin && (url.pathname.endsWith('.html') || url.pathname.endsWith('.css') || @@ -144,13 +235,13 @@ function isAppAsset(url) { // ============================================================================ /** - * Cache First - Use cache, fallback to network - * Best for: App shell, static assets + * Cache First — Use cache, fallback to network. + * Best for: App shell, static assets. */ async function cacheFirst(request, cacheName) { const cached = await caches.match(request); if (cached) return cached; - + try { const response = await fetch(request); if (response.ok) { @@ -159,7 +250,6 @@ async function cacheFirst(request, cacheName) { } return response; } catch (error) { - // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline.html'); } @@ -168,8 +258,8 @@ async function cacheFirst(request, cacheName) { } /** - * Network First - Try network, fallback to cache - * Best for: API requests, dynamic content + * Network First — Try network, fallback to cache. + * Best for: API requests, dynamic content. */ async function networkFirst(request, cacheName) { try { @@ -187,89 +277,141 @@ async function networkFirst(request, cacheName) { } /** - * Stale While Revalidate - Return cache immediately, update in background - * Best for: Module assets, frequently updated content + * Stale While Revalidate — Return cache immediately, update in background. + * Best for: Module assets, frequently updated content. */ async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); - + const fetchPromise = fetch(request).then((response) => { if (response.ok) { cache.put(request, response.clone()); } return response; }).catch(() => cached); - + return cached || fetchPromise; } /** - * Cache Then Network with limit - Cache tiles with size limit - * Best for: Map tiles + * Tile Cache then Network — Per-host bucket with size limit. + * Cache first; on miss, fetch from network and store. + * + * Memory-conservative eviction: + * • Increments an in-memory counter on every successful insert + * • Only calls cache.keys() (which materialises all Request objects) every + * EVICTION_CHECK_INTERVAL inserts — so the cost is amortised + * • Eviction drops the oldest 10 % when over the per-host limit + * + * On network failure (offline), serves a 408 so the map renders a blank tile + * rather than throwing. */ -async function cacheThenNetwork(request, cacheName, maxItems) { +async function tileCacheThenNetwork(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); - if (cached) return cached; - + try { const response = await fetch(request); - + if (response.ok) { - // Check cache size and trim if needed - const keys = await cache.keys(); - if (keys.length >= maxItems) { - // Remove oldest entries (first 10%) - const toDelete = keys.slice(0, Math.ceil(maxItems * 0.1)); - await Promise.all(toDelete.map(key => cache.delete(key))); + // Bump the counter; periodically run a real eviction sweep + const count = (_tileInsertCounters.get(cacheName) || 0) + 1; + _tileInsertCounters.set(cacheName, count); + + if (count % EVICTION_CHECK_INTERVAL === 0) { + // Reset the counter — next sweep is another EVICTION_CHECK_INTERVAL away + _tileInsertCounters.set(cacheName, 0); + await maybeEvict(cache, cacheName); } - - cache.put(request, response.clone()); + + // Don't await put() — it can run after we return the response, keeping + // the fetch hot path lightweight. + cache.put(request, response.clone()).catch((err) => { + // QuotaExceededError → run an immediate eviction sweep and retry once + if (err && err.name === 'QuotaExceededError') { + maybeEvict(cache, cacheName, /* force */ true).catch(() => {}); + } + }); } - + return response; } catch (error) { - // For tiles, just fail silently - map will show blank tile + // Offline — let the map renderer show a blank tile return new Response('', { status: 408, statusText: 'Offline' }); } } +/** + * Run an eviction sweep on a cache, dropping the oldest 10 % of entries + * when over the per-cache limit. Heavy: only call periodically. + */ +async function maybeEvict(cache, cacheName, force = false) { + try { + const limit = TILE_LIMITS[cacheName] || 1500; + const keys = await cache.keys(); + if (force || keys.length >= limit) { + const drop = Math.max(1, Math.ceil(limit * 0.1)); + const toDelete = keys.slice(0, drop); + await Promise.all(toDelete.map((k) => cache.delete(k))); + } + } catch (err) { + console.warn('[SW] eviction sweep failed for', cacheName, err); + } +} + // ============================================================================ // MESSAGE HANDLING // ============================================================================ self.addEventListener('message', (event) => { const { type, payload } = event.data || {}; - + switch (type) { case 'SKIP_WAITING': self.skipWaiting(); break; - + case 'CACHE_MODULES': cacheModules(payload.modules); break; - + case 'CLEAR_USER_CACHE': clearUserCaches(); break; - + case 'GET_CACHE_STATUS': - getCacheStatus().then(status => { + getCacheStatus().then((status) => { event.source.postMessage({ type: 'CACHE_STATUS', status }); }); break; + + // ----- Tile-cache management (Phase 1 offline maps) ----- + case 'GET_TILE_STATS': + getTileStats().then((stats) => { + event.source.postMessage({ type: 'TILE_STATS', stats }); + }); + break; + + case 'CLEAR_TILE_CACHES': + clearTileCaches().then(() => { + event.source.postMessage({ type: 'TILE_CACHES_CLEARED' }); + }); + break; } }); +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + /** - * Cache specific modules on demand + * Cache specific modules on demand. */ async function cacheModules(moduleNames) { const cache = await caches.open(MODULES_CACHE); - + for (const moduleName of moduleNames) { try { const moduleAssets = [ @@ -277,9 +419,8 @@ async function cacheModules(moduleNames) { `/modules/${moduleName}/index.css`, `/modules/${moduleName}/index.html` ]; - + await cache.addAll(moduleAssets.filter(async (url) => { - // Only cache assets that exist try { const response = await fetch(url, { method: 'HEAD' }); return response.ok; @@ -287,7 +428,7 @@ async function cacheModules(moduleNames) { return false; } })); - + console.log('[SW] Cached module:', moduleName); } catch (error) { console.warn('[SW] Failed to cache module:', moduleName, error); @@ -296,7 +437,8 @@ async function cacheModules(moduleNames) { } /** - * Clear user-specific caches (call on logout) + * Clear user-specific caches (call on logout). + * Tile caches are NOT cleared here — those belong to the device, not the user. */ async function clearUserCaches() { await caches.delete(API_CACHE); @@ -305,17 +447,90 @@ async function clearUserCaches() { } /** - * Get cache status information + * Get summary status of all caches (count of entries in each). */ async function getCacheStatus() { const cacheNames = await caches.keys(); const status = {}; - + for (const name of cacheNames) { const cache = await caches.open(name); const keys = await cache.keys(); status[name] = keys.length; } - + return status; } + +/** + * Get per-provider tile cache statistics. + * + * Returns shape: + * { + * totals: { count, estBytes }, + * byProvider: [{ key, label, count, limit, estBytes }, ...] + * } + * + * estBytes is an approximation (count × AVG_TILE_BYTES). For an exact size, + * the caller can use navigator.storage.estimate() on the page side. + * + * Result is cached for STATS_TTL_MS so rapid re-queries (e.g. multiple + * Settings opens) don't re-enumerate every cache. + */ +const STATS_TTL_MS = 10 * 1000; +let _cachedStats = null; +let _cachedStatsAt = 0; + +async function getTileStats({ force = false } = {}) { + const now = Date.now(); + if (!force && _cachedStats && (now - _cachedStatsAt) < STATS_TTL_MS) { + return _cachedStats; + } + + const byProvider = []; + let totalCount = 0; + + for (const cacheName of ALL_TILE_CACHES) { + let count = 0; + if (await caches.has(cacheName)) { + const cache = await caches.open(cacheName); + // matchAll returns a smaller payload than keys() on Safari, but neither + // is free. Done at most once per STATS_TTL_MS thanks to the cache above. + const keys = await cache.keys(); + count = keys.length; + } + byProvider.push({ + key: cacheName, + label: TILE_CACHE_LABELS[cacheName] || cacheName, + count, + limit: TILE_LIMITS[cacheName] || 0, + estBytes: count * AVG_TILE_BYTES, + }); + totalCount += count; + } + + _cachedStats = { + totals: { + count: totalCount, + estBytes: totalCount * AVG_TILE_BYTES, + }, + byProvider, + }; + _cachedStatsAt = now; + return _cachedStats; +} + +/** + * Delete every tile cache. Frees the device storage used by cached map tiles. + * Does not affect app-shell, modules, or API caches. + */ +async function clearTileCaches() { + const results = await Promise.all( + ALL_TILE_CACHES.map((name) => caches.delete(name)) + ); + // Reset counters and invalidate stats cache + _tileInsertCounters.clear(); + _cachedStats = null; + _cachedStatsAt = 0; + console.log('[SW] Cleared tile caches:', ALL_TILE_CACHES.filter((_, i) => results[i])); +} diff --git a/dist/icons/luspa.icon/Assets/luspalogo.png b/favicon.ico/Assets/luspalogo.png similarity index 100% rename from dist/icons/luspa.icon/Assets/luspalogo.png rename to favicon.ico/Assets/luspalogo.png diff --git a/dist/icons/luspa.icon/icon.json b/favicon.ico/icon.json similarity index 100% rename from dist/icons/luspa.icon/icon.json rename to favicon.ico/icon.json diff --git a/index.html b/index.html index 3458695..8d7a11c 100644 --- a/index.html +++ b/index.html @@ -2,19 +2,64 @@ - + - - - + + + + - - - - + + LUPMIS2 Drawing Tools @@ -106,6 +151,295 @@ --radius-2xl: 1rem; } + /* ─── Fieldwork Mode ─── high-contrast + larger touch targets ─── */ + .fieldwork-mode { + --foreground: #000; + --background: #fff; + --card: #fff; + --card-foreground: #000; + --primary: #0044aa; + --primary-foreground: #fff; + --primary-hover: #003080; + --muted: #e0e0e0; + --muted-foreground: #333; + --accent: #cce0ff; + --accent-foreground: #000; + --border: rgba(0,0,0,0.25); + --success: #005a00; + --success-foreground: #fff; + --warning: #b36b00; + --warning-foreground: #000; + --destructive: #b80000; + --destructive-foreground: #fff; + --ring: #0044aa; + --bs-body-color: #000; + } + + /* Fieldwork: larger dock buttons */ + .fieldwork-mode .dock-btn { + min-width: 72px; + min-height: 58px; + font-size: 1.6rem; + border-width: 2px; + } + .fieldwork-mode .dock-btn-label { + font-size: 0.75rem; + font-weight: 600; + } + + /* Fieldwork: bolder navbar */ + .fieldwork-mode .navbar { + border-bottom-width: 4px; + } + .fieldwork-mode .navbar .navbar-brand { + font-size: 1.6rem; + } + + /* Fieldwork: larger offcanvas toggle buttons */ + .fieldwork-mode .offcanvas-toggle { + width: 44px; + height: 44px; + font-size: 1.2rem; + } + + /* Fieldwork: thicker bottom dock border */ + .fieldwork-mode .bottom-dock { + border-top-width: 4px; + } + + /* Fieldwork: larger text in cards / lists */ + .fieldwork-mode .card-header h6 { + font-size: 1rem; + } + .fieldwork-mode .list-group-item { + font-size: 0.95rem; + padding: 0.65rem 1rem; + } + + /* Fieldwork: larger buttons globally */ + .fieldwork-mode .btn { + font-size: 0.95rem; + padding: 0.5rem 1rem; + font-weight: 600; + } + .fieldwork-mode .btn-sm { + font-size: 0.85rem; + padding: 0.4rem 0.75rem; + } + + /* Fieldwork: stronger borders on inputs / form controls */ + .fieldwork-mode .form-control, + .fieldwork-mode .form-select { + border-width: 2px; + border-color: #555; + font-size: 1rem; + } + + /* Fieldwork: bolder map controls (ol-ext) */ + .fieldwork-mode .ol-control button { + font-size: 1.3rem; + width: 2.2em; + height: 2.2em; + } + + /* Fieldwork: scale bar text legibility */ + .fieldwork-mode .ol-scale-bar .ol-scale-step-text, + .fieldwork-mode .ol-scale-bar .ol-scale-text { + font-size: 12px; + font-weight: 700; + text-shadow: 0 0 4px #fff, 0 0 8px #fff; + } + + /* ─── Dark Mode ─── reversed colour scheme ─── */ + .dark-mode { + --foreground: #e0dff0; + --background: #131325; + --card: #1e1e38; + --card-foreground: #e0dff0; + --primary: #4d9de6; + --primary-foreground: #fff; + --primary-hover: #6fb3f0; + --muted: #272745; + --muted-foreground: #9594a8; + --accent: #1e3a5f; + --accent-foreground: #e0dff0; + --border: rgba(255,255,255,0.12); + --ring: #4d9de6; + --success: #2dd46a; + --success-foreground: #131325; + --warning: #ffb84d; + --warning-foreground: #131325; + --destructive: #f04040; + --destructive-foreground: #fff; + --bs-body-color: #e0dff0; + --bs-body-bg: #131325; + --bs-tertiary-bg: #1e1e38; + color-scheme: dark; + } + + /* Dark: navbar */ + .dark-mode .navbar { + background-color: #1a1a30 !important; + box-shadow: 0 1px 6px rgba(0,0,0,0.4); + } + + /* Dark: bottom dock */ + .dark-mode .bottom-dock { + background-color: #1a1a30; + box-shadow: 0 -2px 10px rgba(0,0,0,0.3); + } + .dark-mode .dock-btn { + border-color: var(--primary); + color: var(--foreground); + } + .dark-mode .dock-btn:hover { + background-color: var(--muted); + } + .dark-mode .dock-btn.active { + background-color: var(--primary); + color: var(--primary-foreground); + } + + /* Dark: offcanvas panels */ + .dark-mode .offcanvas { + background-color: var(--background) !important; + color: var(--foreground) !important; + } + .dark-mode .offcanvas-header { + border-bottom-color: var(--border) !important; + } + .dark-mode .btn-close { + filter: invert(1) grayscale(100%) brightness(200%); + } + + /* Dark: cards */ + .dark-mode .card { + background-color: var(--card) !important; + color: var(--card-foreground) !important; + border-color: var(--border) !important; + } + + /* Dark: offcanvas toggle buttons */ + .dark-mode .offcanvas-toggle { + background-color: var(--card); + color: var(--foreground); + border-color: var(--border); + } + .dark-mode .offcanvas-toggle:hover { + background-color: var(--primary); + color: var(--primary-foreground); + } + + /* Dark: form controls */ + .dark-mode .form-control, + .dark-mode .form-select { + background-color: var(--muted) !important; + color: var(--foreground) !important; + border-color: var(--border) !important; + } + .dark-mode .form-check-input { + background-color: var(--muted); + border-color: var(--muted-foreground); + } + .dark-mode .form-check-input:checked { + background-color: var(--primary); + border-color: var(--primary); + } + + /* Dark: list groups */ + .dark-mode .list-group-item { + background-color: var(--card) !important; + color: var(--card-foreground) !important; + border-color: var(--border) !important; + } + + /* Dark: buttons */ + .dark-mode .btn-outline-primary { + color: var(--primary); + border-color: var(--primary); + } + .dark-mode .btn-outline-danger { + color: var(--destructive); + border-color: var(--destructive); + } + + /* Dark: text utilities */ + .dark-mode .text-muted { + color: var(--muted-foreground) !important; + } + + /* Dark: measurement tooltips */ + .dark-mode .measure-tooltip { + background: rgba(30, 30, 56, 0.95); + color: var(--foreground); + border-color: var(--primary); + } + .dark-mode .measure-tooltip::before { + border-right-color: var(--primary); + } + + /* Dark: OL controls */ + .dark-mode .ol-control button { + background-color: var(--card) !important; + color: var(--foreground) !important; + } + .dark-mode .ol-control button:hover { + background-color: var(--primary) !important; + color: var(--primary-foreground) !important; + } + .dark-mode .ol-attribution, + .dark-mode .ol-attribution a { + color: var(--muted-foreground) !important; + } + + /* Dark: scale bar */ + .dark-mode .ol-scale-bar .ol-scale-step-text, + .dark-mode .ol-scale-bar .ol-scale-text { + color: #fff !important; + text-shadow: 0 0 4px #000, 0 0 8px #000 !important; + } + .dark-mode .ol-scale-bar .ol-scale-singlebar-even { + background-color: #fff !important; + } + .dark-mode .ol-scale-bar .ol-scale-singlebar-odd { + background-color: #999 !important; + } + + /* Dark: map drop overlay */ + .dark-mode .map-drop-overlay { + background: rgba(19, 19, 37, 0.85); + border-color: var(--primary); + color: var(--foreground); + } + + /* Dark: ol-ext LayerSwitcher */ + .dark-mode .ol-layerswitcher { + background-color: var(--card) !important; + } + .dark-mode .ol-layerswitcher .panel { + background-color: var(--card) !important; + color: var(--foreground) !important; + } + .dark-mode .ol-layerswitcher .panel li { + color: var(--foreground); + } + .dark-mode .ol-layerswitcher .ol-switchertopdiv, + .dark-mode .ol-layerswitcher .ol-switcherbottomdiv { + background: var(--card) !important; + } + + /* Dark: alert boxes */ + .dark-mode .alert-danger { + background-color: rgba(240, 64, 64, 0.15) !important; + color: var(--destructive) !important; + border-color: var(--destructive) !important; + } + .dark-mode .alert-success { + background-color: rgba(45, 212, 106, 0.15) !important; + color: var(--success) !important; + border-color: var(--success) !important; + } + /* Full height layout */ html, body { height: 100%; @@ -214,9 +548,12 @@ font-family: var(--font-body); } - /* Main container - full height */ + /* Main container - full height. + 100dvh accounts for mobile browser chrome and OS nav bars. + Falls back to 100vh for older browsers. */ .app-container { height: 100vh; + height: 100dvh; display: flex; flex-direction: column; } @@ -244,6 +581,35 @@ height: 100%; } + /* Drag-and-drop overlay shown when files are dragged over the map */ + .map-drop-overlay { + position: absolute; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 94, 184, 0.15); + border: 3px dashed var(--primary, #005eb8); + border-radius: 8px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + } + .map-container.drag-over .map-drop-overlay { + opacity: 1; + } + .map-drop-overlay span { + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 1.15rem; + font-weight: 600; + color: var(--primary, #005eb8); + background: var(--card, #fff); + padding: 0.6rem 1.4rem; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.12); + } + /* Offline indicator */ #offline-indicator { display: none; @@ -301,9 +667,14 @@ z-index: 1050; } - /* Fix ol-ext LayerSwitcher z-index */ - .ol-layerswitcher { - z-index: 100; + /* OL controls stacking context fix — OpenLayers sets z-index:0 on + .ol-overlaycontainer-stopevent, trapping all controls below the + offcanvas-toggle buttons (z-index:500). Raising the container + to 501 lets the LayerSwitcher dropdown render above the toggles. + pointer-events:none on the container still lets clicks through + to the toggle buttons underneath. */ + .ol-overlaycontainer-stopevent { + z-index: 501 !important; } /* Alert hint box */ @@ -419,7 +790,7 @@ } .offcanvas-toggle-bottom { - bottom: 80px; /* Above the dock */ + bottom: calc(80px + env(safe-area-inset-bottom, 0px)); /* Above the dock */ left: 50%; transform: translateX(-50%); } @@ -432,7 +803,10 @@ transform: translateX(-50%) scale(0.95); } - /* Bottom Dock — white card style with blue-strong accent */ + /* Bottom Dock — white card style with blue-strong accent. + env(safe-area-inset-bottom) adds padding on devices with a + home indicator / gesture bar (e.g. iPhone notch models). + The value is 0 on devices without an inset. */ .bottom-dock { position: absolute; bottom: 0; @@ -441,7 +815,7 @@ z-index: 600; background-color: var(--card); border-top: 3px solid var(--primary); - padding: 8px 16px; + padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); display: flex; justify-content: space-around; align-items: center; @@ -503,6 +877,13 @@ justify-content: center; } + /* Snap-guides toggle — highlighted when active */ + .ol-snap-toggle.ol-active button { + background: var(--primary) !important; + color: var(--primary-foreground, #fff) !important; + border-radius: 3px; + } + /* Touch-friendly improvements for forms and buttons */ .form-control, .form-select { min-height: 44px; @@ -541,6 +922,18 @@ color: var(--foreground); } + /* Message log in the right panel */ + .message-log { + max-height: 260px; + overflow-y: auto; + } + + .message-log-entry { + font-size: 0.82rem; + border-color: var(--border, #eee) !important; + background: transparent; + } + /* ol-ext GeolocationButton styling */ .ol-geobt { top: auto !important; @@ -830,6 +1223,7 @@ /* Locations list in offcanvas - can be taller now without form */ .offcanvas-end .locations-list { max-height: calc(100vh - 280px); + max-height: calc(100dvh - 280px); overflow-y: auto; } @@ -921,17 +1315,32 @@ font-size: 18px; } - /* ScaleLine - position above the bottom dock */ - .ol-scale-line { - bottom: 76px !important; + /* ScaleBar - position above the bottom dock with 4px gap */ + .ol-scale-bar { + bottom: calc(85px + env(safe-area-inset-bottom, 0px)) !important; left: 10px !important; } - .ol-scale-line-inner { - border-color: var(--foreground) !important; + .ol-scale-bar .ol-scale-step-text { color: var(--foreground) !important; font-family: var(--font-body) !important; font-size: 11px !important; + text-shadow: 0 0 3px var(--background), 0 0 6px var(--background) !important; + } + + .ol-scale-bar .ol-scale-text { + color: var(--foreground) !important; + font-family: var(--font-body) !important; + font-size: 11px !important; + text-shadow: 0 0 3px var(--background), 0 0 6px var(--background) !important; + } + + .ol-scale-bar .ol-scale-singlebar-even { + background-color: var(--foreground) !important; + } + + .ol-scale-bar .ol-scale-singlebar-odd { + background-color: var(--muted-foreground) !important; } /* ol-ext Bar overrides */ @@ -980,6 +1389,7 @@
+
Drop file to import (.shp .geojson .kml)
+ + + + + + + +
-
+
Local Database Tables
+
TableRecordsRecords
- + + @@ -1142,6 +1575,10 @@ + + + +
+
+
Messages
+ +
+
+
+
+ No messages yet. +
+
+
+
+
-
Bottom Panel
+
Settings
-
+
-

This is the bottom offcanvas panel.

-

You can add a data table, charts, or other wide content here.

+
+ +
+
+
+
+
+
Fieldwork Mode
+ High-contrast colours and larger touch targets for bright sunlight and field conditions. +
+
+ +
+
+
+ +
+
+
+
+
+
Dark Mode
+ Reduce glare and save battery with a dark colour scheme. +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Measurement System
+ Switch between Metric (m, km) and Imperial (ft, mi, acres) units. +
+
+ + +
+
+
+
+
+ +
+
+
+
+
+
Default Base Map
+ Base map shown on app start. Saved on this device. +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Offline Map Tiles +
+ + Map tiles you've already viewed are cached on this device so they work offline. + Tiles are cached automatically as you browse, or you can pre-download a region. + +
+
+ + +
+
+
+
Loading…
+
+
+
+
+
+
+ + + + diff --git a/main.js b/main.js index 21767c0..07e728d 100644 --- a/main.js +++ b/main.js @@ -36,6 +36,11 @@ import { insertNewParcel, saveBuildingFootprints, getLocalBuildingFootprints, + saveOSMRoads, + getLocalOSMRoads, + isCachedLayerTable, + clearTable, + clearAllCachedLayers, getTableStats, getTableContent } from './src/database.js'; @@ -49,14 +54,35 @@ import GeoJSON from 'ol/format/GeoJSON'; // OpenLayers WKT format (for writing drawn polygon geometries to database) import WKT from 'ol/format/WKT'; +// OpenLayers KML format (for KML file import) +import KML from 'ol/format/KML'; + +// Shapefile parser (reads .zip containing .shp/.dbf/.shx/.prj) +// Lazy-loaded — only fetched the first time the user imports a shapefile. +let _shpModule = null; +async function getShp() { + if (!_shpModule) { + const mod = await import('shpjs'); + _shpModule = mod.default || mod; + } + return _shpModule; +} + // Map measurement and drawing tools import { MapTools } from './src/components/MapTools.js'; // PWA module (registers Service Worker, handles install/offline) -import { initPWA, isOnline, onOfflineChange } from './src/pwa.js'; +import { initPWA, isOnline, onOfflineChange, getTileCacheStats, clearTileCaches, getStorageEstimate, onServiceWorkerControllerChange } from './src/pwa.js'; +import { + BASEMAP_TEMPLATES, + GHANA_EXTENT_3857, + countTiles, + estimatedSizeBytes, + OfflineTileDownloader, +} from './src/offlineTiles.js'; // Remote database API (PostgreSQL backend) -import { getDistrictBoundary, getLayers, getCollectorZones, getDistrictParcels, getBuildingFootprints } from './src/remotedb.js'; +import { checkServerReachable, isServerReachable, getDistrictBoundary, getLayers, getCollectorZones, getDistrictParcels, getBuildingFootprints, getContoursHillshade, getOSMRoads } from './src/remotedb.js'; // Map instance (global for access across functions) let mapView = null; @@ -80,10 +106,13 @@ async function initApp() { }); // 2. Initialize the map + // Restore the user's preferred default base map from localStorage + const savedBasemap = localStorage.getItem('default-basemap') || 'topo'; + mapView = new MapView('map', { center: [-1.5, 7.5], // Ghana zoom: 7, - basemap: 'osm' + basemap: savedBasemap, }); // Initialize map measurement tools @@ -93,9 +122,13 @@ async function initApp() { mapTools.onMeasureComplete((result) => { console.log('[MapTools] Measurement complete:', result); - // When an area polygon is completed, show the attribute form popup + // Only show the Polygon Attributes popup for polygons drawn with the + // Draw tool — NOT for area measurements (which have _layerType = 'measure_area'). if (result.type === 'polygon' && result.coordinate) { - mapView?.showDrawnPolygonPopup(result.feature, result.coordinate); + const lt = result.feature?.get('_layerType'); + if (lt !== 'measure_area') { + mapView?.showDrawnPolygonPopup(result.feature, result.coordinate); + } } }); @@ -108,10 +141,9 @@ async function initApp() { console.log('[MapClick] Clicked at:', lon.toFixed(4), lat.toFixed(4)); console.log('[MapClick] currentMode =', currentMode); - // In draw mode the Select interaction handles clicks for geometry - // editing — don't interfere with it. - if (currentMode === 'draw') { - console.log('[MapClick] In draw mode, Select interaction handles clicks'); + // In draw or measurement modes, clicks drive the tool — don't + // open popups or select features. + if (currentMode === 'draw' || currentMode.startsWith('measure')) { return; } @@ -164,6 +196,9 @@ async function initApp() { } else if (layerType === 'measure_circle_radius') { // Clicked on the radius line — ignore return; + } else if (layerType === 'measure_area') { + // Area measurement polygon: show intersection analysis + mapView.showAreaIntersectionPopup(feature, evt.coordinate); } else if (layerType === 'collector_zone') { mapView.showInfoPopup(feature, evt.coordinate, { title: 'Zone Info', @@ -265,6 +300,17 @@ async function initApp() { const status = await getDatabaseStatus(); console.log('[App] Database status:', status); + // Quick server reachability check (5 s timeout) — if the API server + // is down, all load functions will skip remote fetches and fall back + // to local cached data immediately, keeping the app responsive. + if (isOnline()) { + const reachable = await checkServerReachable(); + if (!reachable) { + console.warn('[App] API server unreachable — using local data only'); + showWarning('Server not responding — loading cached data.'); + } + } + // Load remote overlays (needs remote_data table from initSchema) // loadLayers must complete first so the layer groups exist // before loadDistrictBoundary adds into the Administration group. @@ -277,6 +323,9 @@ async function initApp() { loadCollectorZones(); loadParcels(); loadBuildingFootprints(); + loadContoursHillshade(); + loadOSMRoads(); + loadExternalWMSLayers(); } catch (error) { console.error('[App] Database initialization failed:', error); @@ -316,6 +365,24 @@ async function initApp() { } }); + // 8. Fieldwork mode (high-contrast + large touch targets) + initFieldworkMode(); + + // 9. Measurement system toggle (metric / imperial) + initMeasurementSystem(); + + // 10. Dark mode + initDarkMode(); + + // 11. Default base map selector + initDefaultBasemap(); + + // 12. Offline tile-cache stats card + initOfflineTileCache(); + + // 13. Offline-download dialog + initOfflineDownloadDialog(); + console.log('[App] Initialized successfully'); } @@ -326,6 +393,9 @@ async function initApp() { function initUI() { console.log('[initUI] Starting UI initialization...'); + // Message log (persistent stack in right panel) + initMessageLog(); + // Export button const exportBtn = document.getElementById('export-btn'); if (exportBtn) { @@ -338,6 +408,31 @@ function initUI() { localDataBtn.addEventListener('click', () => refreshLocalDataStats()); } + // File import buttons (Shapefile, GeoJSON, KML) + const importShpBtn = document.getElementById('import-shp-btn'); + const shpFileInput = document.getElementById('shp-file-input'); + if (importShpBtn && shpFileInput) { + importShpBtn.addEventListener('click', () => shpFileInput.click()); + shpFileInput.addEventListener('change', handleShapefileImport); + } + + const importGeoJSONBtn = document.getElementById('import-geojson-btn'); + const geojsonFileInput = document.getElementById('geojson-file-input'); + if (importGeoJSONBtn && geojsonFileInput) { + importGeoJSONBtn.addEventListener('click', () => geojsonFileInput.click()); + geojsonFileInput.addEventListener('change', handleGeoJSONImport); + } + + const importKMLBtn = document.getElementById('import-kml-btn'); + const kmlFileInput = document.getElementById('kml-file-input'); + if (importKMLBtn && kmlFileInput) { + importKMLBtn.addEventListener('click', () => kmlFileInput.click()); + kmlFileInput.addEventListener('change', handleKMLImport); + } + + // Drag-and-drop file import on the map + initMapDropZone(); + // GeoJSON Export button const exportGeoJSONBtn = document.getElementById('exportGeoJSON-btn'); if (exportGeoJSONBtn) { @@ -669,32 +764,95 @@ function renderLocations(locations) { async function refreshLocalDataStats() { const statsContainer = document.getElementById('local-data-stats'); const tbody = document.getElementById('local-data-tbody'); + const clearAllBtn = document.getElementById('clear-all-cached-btn'); if (!statsContainer || !tbody) return; try { const stats = await getTableStats(); - tbody.innerHTML = stats.map(t => ` - - - - - `).join(''); + + tbody.innerHTML = stats.map((t) => { + const isCached = isCachedLayerTable(t.name); + const clearBtn = isCached + ? `` + : ''; + return ` + + + + + + `; + }).join(''); statsContainer.classList.remove('d-none'); - // Attach click handlers to table name links - tbody.querySelectorAll('.table-name-link').forEach(link => { + // Table-name link → open content modal + tbody.querySelectorAll('.table-name-link').forEach((link) => { link.addEventListener('click', (e) => { e.preventDefault(); showTableContent(link.dataset.table); }); }); + + // Per-row clear → confirm, clear that table, refresh stats + tbody.querySelectorAll('.table-clear-btn').forEach((btn) => { + btn.addEventListener('click', async (e) => { + e.preventDefault(); + const tableName = btn.dataset.table; + if (!confirm(`Clear local cache for "${tableName}"?\n\nThe data will be re-downloaded from the server on the next app start.`)) return; + try { + const removed = await clearTable(tableName); + showSuccess(`Cleared ${removed} row${removed === 1 ? '' : 's'} from "${tableName}". It will re-download on next start.`); + await refreshLocalDataStats(); + } catch (err) { + console.error('[App] Per-table clear failed:', err); + showError(`Could not clear "${tableName}": ${err.message}`); + } + }); + }); } catch (error) { console.error('[App] Failed to load table stats:', error); - tbody.innerHTML = ``; + tbody.innerHTML = ``; statsContainer.classList.remove('d-none'); } + + // Bulk-clear button — wire up once + if (clearAllBtn && !clearAllBtn._wired) { + clearAllBtn._wired = true; + clearAllBtn.addEventListener('click', handleClearAllCachedLayers); + } +} + +/** + * Clear every cached layer table and offer to reload the app so the layers + * re-download immediately. If the user dismisses the reload prompt, the + * fresh fetch will happen on the next manual app start. + */ +async function handleClearAllCachedLayers() { + if (!confirm( + 'Delete all cached map layers from this device?\n\n' + + 'The next time the app starts (or after a reload), every layer will be ' + + 're-downloaded from the server. Your locally drawn data is not affected.' + )) return; + + try { + const results = await clearAllCachedLayers(); + const total = results.reduce((s, r) => s + r.count, 0); + showSuccess(`Cleared ${total} row${total === 1 ? '' : 's'} across ${results.length} table${results.length === 1 ? '' : 's'}.`); + await refreshLocalDataStats(); + + if (confirm('Reload the app now to re-download the layers fresh from the server?')) { + window.location.reload(); + } + } catch (err) { + console.error('[App] Clear-all failed:', err); + showError('Failed to clear cached layers: ' + err.message); + } } // ============================================================================ @@ -1040,8 +1198,8 @@ async function loadDistrictBoundary() { zoomToBoundary(layer); } - // Step 2: If online, fetch fresh data from the API - if (isOnline()) { + // Step 2: If online and server reachable, fetch fresh data from the API + if (isOnline() && isServerReachable()) { console.log('[App] Fetching district boundary from API...'); const apiResponse = await getDistrictBoundary(); @@ -1134,8 +1292,8 @@ async function loadCollectorZones() { } } - // Step 2: If online, fetch fresh data from the API - if (isOnline()) { + // Step 2: If online and server reachable, fetch fresh data from the API + if (isOnline() && isServerReachable()) { console.log('[App] Fetching collector zones from API...'); const apiResponse = await getCollectorZones(); @@ -1179,17 +1337,31 @@ async function loadCollectorZones() { function parcelsToGeoJSON(parcels) { if (!Array.isArray(parcels) || parcels.length === 0) return null; + // Deduplicate by id — the API may return the same parcel more than once + const seen = new Set(); const features = []; for (const parcel of parcels) { - // API may use 'polygon', 'boundary', 'geom' or 'wkt' for the WKT field - const wkt = parcel.polygon || parcel.boundary || parcel.geom || parcel.wkt; - const geometry = parseWKT(wkt); + const id = parcel.id || parcel.parcelid || parcel.parcel_id; + if (id != null) { + if (seen.has(id)) continue; + seen.add(id); + } + + // Prefer the GeoJSON geometry (sp_boundary) if available; fall back to WKT + let geometry = null; + if (parcel.sp_boundary && parcel.sp_boundary.type && parcel.sp_boundary.coordinates) { + geometry = { type: parcel.sp_boundary.type, coordinates: parcel.sp_boundary.coordinates }; + } else { + const wkt = parcel.boundary || parcel.polygon || parcel.geom || parcel.wkt; + geometry = parseWKT(wkt); + } if (!geometry) continue; - // Collect all properties except the raw WKT geometry + // Collect all properties except bulky geometry fields + const skipKeys = new Set(['polygon', 'boundary', 'geom', 'wkt', 'textboundary', 'sp_boundary']); const properties = { _layerType: 'parcel' }; for (const [key, value] of Object.entries(parcel)) { - if (key === 'polygon' || key === 'boundary' || key === 'geom' || key === 'wkt') continue; + if (skipKeys.has(key)) continue; properties[key] = value; } @@ -1258,8 +1430,8 @@ async function loadParcels() { } } - // Step 2: If online, fetch fresh data from the API - if (isOnline()) { + // Step 2: If online and server reachable, fetch fresh data from the API + if (isOnline() && isServerReachable()) { console.log('[App] Fetching parcels from API...'); const apiResponse = await getDistrictParcels(); @@ -1395,8 +1567,8 @@ async function loadBuildingFootprints() { } } - // Step 2: If online, fetch fresh data from the API - if (isOnline()) { + // Step 2: If online and server reachable, fetch fresh data from the API + if (isOnline() && isServerReachable()) { console.log('[App] Fetching building footprints from API...'); const apiResponse = await getBuildingFootprints(); @@ -1435,6 +1607,281 @@ async function loadBuildingFootprints() { } } +/** + * Convert an array of DB rows (each with a WKT geom field) to GeoJSON. + * Uses OpenLayers' WKT parser so LINESTRING, MULTILINESTRING, POLYGON, etc. + * are all supported out of the box. + * + * @param {Array} rows — API rows, each having a WKT-valued geom/geometry/wkt field + * @param {string} layerType — value to store in each feature's _layerType property + * @returns {Object|null} GeoJSON FeatureCollection, or null if no valid rows + */ +function wktRowsToGeoJSON(rows, layerType) { + if (!Array.isArray(rows) || rows.length === 0) return null; + + const wktFormat = new WKT(); + const geojsonFormat = new GeoJSON(); + // Field-name fallbacks — different endpoints alias the geometry column + // differently (e.g. get_osm_roads uses `road`, get_contours_hillshade uses + // `geom`). The first non-null match wins. + const geomKeys = ['geom', 'geometry', 'wkt', 'polygon', 'boundary', 'road', 'line']; + + const features = []; + for (const row of rows) { + const raw = row.geom || row.geometry || row.wkt || row.polygon || row.boundary || row.road || row.line; + if (!raw) continue; + + let olGeom; + try { + if (typeof raw === 'object' && raw !== null && raw.type) { + // Already a GeoJSON geometry — just pass through + features.push({ + type: 'Feature', + properties: flattenProps(row, geomKeys, layerType), + geometry: raw, + }); + continue; + } + olGeom = wktFormat.readGeometry(raw); + } catch (err) { + console.warn(`[App] Could not parse WKT for ${layerType}:`, err, raw?.toString().slice(0, 60)); + continue; + } + + const geometry = JSON.parse(geojsonFormat.writeGeometry(olGeom)); + features.push({ + type: 'Feature', + properties: flattenProps(row, geomKeys, layerType), + geometry, + }); + } + + if (features.length === 0) return null; + return { type: 'FeatureCollection', features }; +} + +/** + * Flatten a DB row into properties, skipping geometry fields and nested objects. + */ +function flattenProps(row, skipKeys, layerType) { + const props = { _layerType: layerType }; + for (const [key, value] of Object.entries(row)) { + if (skipKeys.includes(key)) continue; + if (typeof value === 'object' && value !== null) continue; + props[key] = value; + } + return props; +} + +/** + * Load the "Contours hillshade" layer — elevation contours derived from + * OpenTopography, stored in PostgreSQL as `contours_hillshade`. + * + * Added to the "Biophysical Environment" LayerGroup, initially not visible. + * No local caching (the server is the source of truth). + */ +async function loadContoursHillshade() { + const contoursStyle = { + strokeColor: '#78716c', // warm grey — traditional contour colour + strokeWidth: 0.8, + fillColor: 'rgba(0,0,0,0)', + }; + + const biophysGroup = mapView?.getLayerGroupByTitle('Biophysical Environment'); + console.log('[App] loadContoursHillshade — group:', biophysGroup ? biophysGroup.get('title') : 'null'); + + // Create empty layer first so it always appears in the LayerSwitcher + const emptyGeoJSON = { type: 'FeatureCollection', features: [] }; + const contoursLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Contours hillshade', contoursStyle, biophysGroup); + if (!contoursLayer) { + console.warn('[App] Could not create Contours hillshade layer'); + return; + } + contoursLayer.setVisible(false); + + // Warn when the user enables the layer but it has no data + contoursLayer.on('change:visible', () => { + if (contoursLayer.getVisible() && contoursLayer.getSource().getFeatures().length === 0) { + showError('No Contours hillshade data available. Connect to the internet to download it.'); + } + }); + + // Fetch from API (only when online and server reachable — no local cache) + if (!isOnline() || !isServerReachable()) { + console.log('[App] Contours hillshade not available — offline or server unreachable'); + return; + } + + try { + console.log('[App] Fetching contours_hillshade from API...'); + const apiResponse = await getContoursHillshade(); + + if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) { + console.warn('[App] getContoursHillshade API response invalid:', apiResponse); + return; + } + + const rows = apiResponse.data; + console.log('[App] Contours hillshade from API:', rows.length, 'rows'); + if (rows.length > 0) { + console.log('[App] First row keys:', Object.keys(rows[0])); + } + + const geojson = wktRowsToGeoJSON(rows, 'contours_hillshade'); + if (!geojson) { + console.warn('[App] Could not convert contours to GeoJSON'); + return; + } + + const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' }); + contoursLayer.getSource().clear(); + contoursLayer.getSource().addFeatures(features); + console.log('[App] Contours hillshade loaded:', features.length, 'features'); + + } catch (error) { + console.error('[App] Failed to load contours_hillshade:', error); + } +} + +/** + * Load the "OSM_roads" layer — OpenStreetMap road network for the district. + * + * Added to the "Physical Infrastructures" LayerGroup (id 5), initially not + * visible — becomes visible when the user toggles it in the LayerSwitcher. + * + * Local-first caching: + * 1. Read from the local `osm_roads` table → render immediately if available + * 2. If online, fetch from the API → overwrite the local table → re-render + */ +async function loadOSMRoads() { + const PHYS_INFRA_GROUP_ID = 5; + // Cartographic road casing: a black outer stroke makes the light-coloured + // inner stroke (the "road body") readable on every base map. + const roadsStyle = { + strokeColor: '#F0F1F0', // inner — road body + strokeWidth: 1.5, + lineCasingColor: '#000000', // outer — black casing + lineCasingWidth: 3.5, + fillColor: 'rgba(0,0,0,0)', + }; + + const physInfraGroup = mapView?.getLayerGroup(PHYS_INFRA_GROUP_ID) || null; + console.log('[App] loadOSMRoads — group:', physInfraGroup ? physInfraGroup.get('title') : 'null'); + + // Create the layer immediately (empty) so it appears in the LayerSwitcher + // even when offline. + const emptyGeoJSON = { type: 'FeatureCollection', features: [] }; + const roadsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'OSM_roads', roadsStyle, physInfraGroup); + if (!roadsLayer) { + console.warn('[App] Could not create OSM_roads layer'); + return; + } + roadsLayer.setVisible(false); + + // Warn only when the layer is enabled AND truly empty AND no source is reachable + roadsLayer.on('change:visible', () => { + if (roadsLayer.getVisible() && roadsLayer.getSource().getFeatures().length === 0) { + showError('No OSM roads available locally. Connect to the internet to download them.'); + } + }); + + /** Replace the layer's features with those parsed from the API/cache rows. */ + function setRoadFeatures(rows) { + const geojson = wktRowsToGeoJSON(rows, 'osm_road'); + if (!geojson) { + console.warn('[App] Could not convert OSM roads to GeoJSON'); + return 0; + } + const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' }); + roadsLayer.getSource().clear(); + roadsLayer.getSource().addFeatures(features); + return features.length; + } + + try { + // Step 1 — local cache (works offline, instant) + const cached = await getLocalOSMRoads(); + if (cached) { + const n = setRoadFeatures(cached); + console.log('[App] OSM_roads loaded from local cache:', n, 'features'); + } + + // Step 2 — fetch fresh from API when online + if (isOnline() && isServerReachable()) { + console.log('[App] Fetching OSM_roads from API...'); + const apiResponse = await getOSMRoads(); + + if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) { + console.warn('[App] getOSMRoads API response invalid:', apiResponse); + return; + } + + const rows = apiResponse.data; + console.log('[App] OSM_roads from API:', rows.length, 'rows'); + if (rows.length > 0) { + console.log('[App] First row keys:', Object.keys(rows[0])); + } + + // Persist to local table so it's available next time offline + await saveOSMRoads(rows); + + const n = setRoadFeatures(rows); + console.log('[App] OSM_roads updated from API:', n, 'features'); + + } else if (!cached) { + console.log('[App] OSM_roads not available — offline and no local cache'); + } + + } catch (error) { + console.error('[App] Failed to load OSM_roads:', error); + } +} + +/** + * Add external WMS/XYZ layers to the map. + * Called after loadLayers() so the target layer groups already exist. + */ +function loadExternalWMSLayers() { + // DEAfrica Coastlines v0.4.0 — annual shorelines & rates of change + // Source: Digital Earth Africa GeoServer + // Latest available version as of 2026: v0.4.0 + mapView?.addWMSLayer( + 'Biophysical Environment', + 'DEAfrica Coastlines v0.4', + 'https://geoserver.digitalearth.africa/geoserver/wms', + 'coastlines:DEAfrica_Coastlines', + { serverType: 'geoserver', visible: false, onlineOnly: true } + ); + + // Note: OpenTopoMap is available as the "Topographic" base map — + // no separate overlay in "Biophysical Environment" needed. + + // Digital Earth Africa — SRTM-derived Slope (30m) + // Shows terrain steepness as a background overlay — hills and valleys stand + // out naturally, reading like a traditional shaded-relief topographic map. + // Service: datacube-ows (not GeoServer). + // Layer 'srtm_deriv' styles: 'style_slope', 'style_mrvbf' (valley bottoms), + // 'style_mrrtf' (ridge tops). + mapView?.addWMSLayer( + 'Biophysical Environment', + 'DEAfrica Slope (SRTM 30m)', + 'https://ows.digitalearth.africa/wms', + 'srtm_deriv', + { + serverType: null, + style: 'style_slope', + visible: false, + opacity: 0.5, + zIndex: -50, + onlineOnly: true, + attributions: + '© Digital Earth Africa — ' + + 'SRTM-derived Slope', + legendUrl: 'https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png', + } + ); +} + /** * Load layer categories from the API and create empty VectorLayers on the map. * Uses local-first caching — reads from SQLite first, then refreshes from API when online. @@ -1466,8 +1913,8 @@ async function loadLayers() { createLayerGroupsOnMap(cached); } - // Step 2: If online, fetch fresh data from the API - if (isOnline()) { + // Step 2: If online and server reachable, fetch fresh data from the API + if (isOnline() && isServerReachable()) { console.log('[App] Fetching layer categories from API...'); const apiResponse = await getLayers(); @@ -1533,6 +1980,420 @@ async function syncData() { console.log('[App] Sync placeholder - implement based on your backend'); } +// ============================================================================ +// File Import (Shapefile, GeoJSON, KML) +// ============================================================================ + +/** All layers added by file imports — shared across formats. */ +const importedFileLayers = []; + +/** Default style for imported layers. */ +const IMPORT_STYLE = { + strokeColor: '#e11d48', + strokeWidth: 2, + fillColor: 'rgba(225,29,72,0.12)', +}; + +/** + * Show an error message inside the left panel's file-import alert area. + */ +function showFileImportError(message) { + logMessage('error', message); + const el = document.getElementById('file-import-alert'); + if (el) { + el.querySelector('.message-text').textContent = message; + el.classList.remove('d-none'); + setTimeout(() => el.classList.add('d-none'), 8000); + } +} + +/** + * Add a GeoJSON FeatureCollection (or array of them) to the map, zoom to + * the data, and refresh the imported-layers info card. + * + * @param {Object|Object[]} geojsonInput - Single FeatureCollection or array + * @param {string} fallbackName - Layer name when the FC has no fileName + * @param {string} tag - Log prefix, e.g. 'ShpImport' + */ +function addImportedGeoJSON(geojsonInput, fallbackName, tag) { + const collections = Array.isArray(geojsonInput) ? geojsonInput : [geojsonInput]; + + let totalFeatures = 0; + for (const fc of collections) { + if (!fc || fc.type !== 'FeatureCollection' || !fc.features?.length) continue; + + const layerName = fc.fileName + ? fc.fileName.replace(/\.[^/.]+$/, '') + : fallbackName; + + const layer = mapView?.addGeoJSONLayer(fc, layerName, IMPORT_STYLE); + if (layer) { + importedFileLayers.push(layer); + totalFeatures += fc.features.length; + } + } + + if (totalFeatures === 0) { + showFileImportError('No features found in the file.'); + return; + } + + console.log(`[${tag}] Added ${totalFeatures} feature(s) from ${collections.length} layer(s)`); + + // Zoom to the last imported layer + const lastLayer = importedFileLayers[importedFileLayers.length - 1]; + if (lastLayer) { + const extent = lastLayer.getSource().getExtent(); + mapView?.getMap().getView().fit(extent, { padding: [50, 50, 50, 50], maxZoom: 18 }); + } + + refreshImportedLayersCard(); +} + +/** + * Rebuild the imported-layers info card in the left panel. + */ +function refreshImportedLayersCard() { + const infoEl = document.getElementById('imported-layers-info'); + if (!infoEl) return; + + if (importedFileLayers.length === 0) { + infoEl.innerHTML = ''; + infoEl.classList.add('d-none'); + return; + } + + infoEl.innerHTML = ` +
+
+
Imported Layers
+ +
+
    +
    `; + + const listEl = infoEl.querySelector('#imported-layers-list'); + importedFileLayers.forEach((l, idx) => { + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center py-2'; + li.innerHTML = `${escapeHtml(l.get('title'))} + + ${l.getSource().getFeatures().length} + + `; + listEl.appendChild(li); + }); + infoEl.classList.remove('d-none'); + + // Per-layer remove buttons + infoEl.querySelectorAll('[data-remove-idx]').forEach(btn => { + btn.addEventListener('click', () => { + removeImportedLayer(Number(btn.dataset.removeIdx)); + }); + }); + + // Remove-all button + infoEl.querySelector('#remove-imported-layers')?.addEventListener('click', () => { + removeImportedLayers(); + }); +} + +/** + * Remove a single imported layer by its index in importedFileLayers. + */ +function removeImportedLayer(idx) { + if (idx < 0 || idx >= importedFileLayers.length) return; + const layer = importedFileLayers[idx]; + const overlayGroup = mapView?.getOverlayGroup(); + if (overlayGroup) { + overlayGroup.getLayers().remove(layer); + } + importedFileLayers.splice(idx, 1); + refreshImportedLayersCard(); + console.log('[FileImport] Removed layer:', layer.get('title')); +} + +/** + * Remove all imported layers from the map and clear the info card. + */ +function removeImportedLayers() { + const overlayGroup = mapView?.getOverlayGroup(); + if (overlayGroup) { + for (const layer of importedFileLayers) { + overlayGroup.getLayers().remove(layer); + } + } + importedFileLayers.length = 0; + refreshImportedLayersCard(); + console.log('[FileImport] All imported layers removed'); +} + +// --------------------------------------------------------------------------- +// Shapefile (.shp / .zip) +// --------------------------------------------------------------------------- + +/** + * Build a lookup of selected files keyed by lowercase extension. + */ +function indexFilesByExtension(files) { + const map = {}; + for (const f of files) { + const ext = f.name.split('.').pop().toLowerCase(); + map[ext] = f; + } + return map; +} + +async function handleShapefileImport(evt) { + const files = evt.target.files; + if (!files || files.length === 0) return; + + const MAX_FILE_SIZE = 200 * 1024 * 1024; + const totalSize = Array.from(files).reduce((s, f) => s + f.size, 0); + if (totalSize > MAX_FILE_SIZE) { + const sizeMB = (totalSize / (1024 * 1024)).toFixed(0); + showFileImportError( + `Files too large (${sizeMB} MB total). Maximum supported size is 200 MB.` + ); + evt.target.value = ''; + return; + } + + try { + let geojson; + let displayName; + const byExt = indexFilesByExtension(files); + + if (byExt.zip) { + const file = byExt.zip; + displayName = file.name.replace(/\.zip$/i, ''); + console.log('[ShpImport] Parsing zip', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)'); + const shp = await getShp(); + geojson = await shp(await file.arrayBuffer()); + + } else if (byExt.shp) { + displayName = byExt.shp.name.replace(/\.shp$/i, ''); + + const required = ['dbf', 'shx', 'prj']; + const missing = required.filter(ext => !byExt[ext]); + if (missing.length > 0) { + showFileImportError('Missing required file(s): ' + missing.map(e => '.' + e).join(', ') + + '. Please select .shp, .dbf, .shx and .prj together.'); + evt.target.value = ''; + return; + } + + const shpObj = {}; + shpObj.shp = await byExt.shp.arrayBuffer(); + shpObj.dbf = await byExt.dbf.arrayBuffer(); + shpObj.prj = await new Response(byExt.prj).text(); + if (byExt.cpg) shpObj.cpg = await new Response(byExt.cpg).text(); + + console.log('[ShpImport] Parsing loose files:', + Object.keys(byExt).map(e => '.' + e).join(', '), + '(' + (byExt.shp.size / 1024).toFixed(1) + ' KB .shp)'); + + const shp = await getShp(); + geojson = await shp(shpObj); + + } else { + showFileImportError('Please select a .zip or at least a .shp file.'); + evt.target.value = ''; + return; + } + + addImportedGeoJSON(geojson, displayName, 'ShpImport'); + } catch (error) { + console.error('[ShpImport] Failed:', error); + showFileImportError('Failed to parse shapefile: ' + error.message); + } + + evt.target.value = ''; +} + +// --------------------------------------------------------------------------- +// GeoJSON (.geojson / .json) +// --------------------------------------------------------------------------- + +async function handleGeoJSONImport(evt) { + const file = evt.target.files?.[0]; + if (!file) return; + + // Guard: reject files larger than 200 MB — JSON.parse cannot reliably + // handle them in a single pass and the browser will freeze or crash. + const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200 MB + if (file.size > MAX_FILE_SIZE) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(0); + showFileImportError( + `File too large (${sizeMB} MB). Maximum supported size is 200 MB. ` + + 'Consider splitting the file into smaller tiles with ogr2ogr or QGIS.' + ); + evt.target.value = ''; + return; + } + + try { + const text = await file.text(); + console.log('[GeoJSONImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)'); + + const parsed = JSON.parse(text); + + // Normalise to a FeatureCollection + let fc; + if (parsed.type === 'FeatureCollection') { + fc = parsed; + } else if (parsed.type === 'Feature') { + fc = { type: 'FeatureCollection', features: [parsed] }; + } else if (parsed.type && parsed.coordinates) { + // Bare geometry object + fc = { type: 'FeatureCollection', features: [{ type: 'Feature', geometry: parsed, properties: {} }] }; + } else { + showFileImportError('The file does not contain valid GeoJSON.'); + evt.target.value = ''; + return; + } + + const displayName = file.name.replace(/\.(geo)?json$/i, ''); + addImportedGeoJSON(fc, displayName, 'GeoJSONImport'); + } catch (error) { + console.error('[GeoJSONImport] Failed:', error); + const sizeMB = (file.size / (1024 * 1024)).toFixed(1); + showFileImportError( + `Failed to import "${file.name}" (${sizeMB} MB): ${error.message}` + ); + } + + evt.target.value = ''; +} + +// --------------------------------------------------------------------------- +// KML (.kml) +// --------------------------------------------------------------------------- + +async function handleKMLImport(evt) { + const file = evt.target.files?.[0]; + if (!file) return; + + const MAX_FILE_SIZE = 200 * 1024 * 1024; + if (file.size > MAX_FILE_SIZE) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(0); + showFileImportError( + `File too large (${sizeMB} MB). Maximum supported size is 200 MB.` + ); + evt.target.value = ''; + return; + } + + try { + const text = await file.text(); + console.log('[KMLImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)'); + + const kmlFormat = new KML({ extractStyles: false }); + const features = kmlFormat.readFeatures(text, { + featureProjection: 'EPSG:3857', + }); + + if (!features || features.length === 0) { + showFileImportError('No features found in the KML file.'); + evt.target.value = ''; + return; + } + + // Convert OL features back to GeoJSON so we can use the shared pipeline + const geojsonFormat = new GeoJSON(); + const fc = JSON.parse(geojsonFormat.writeFeatures(features, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:4326', + })); + + const displayName = file.name.replace(/\.kml$/i, ''); + addImportedGeoJSON(fc, displayName, 'KMLImport'); + } catch (error) { + console.error('[KMLImport] Failed:', error); + const sizeMB = (file.size / (1024 * 1024)).toFixed(1); + showFileImportError( + `Failed to import "${file.name}" (${sizeMB} MB): ${error.message}` + ); + } + + evt.target.value = ''; +} + +// --------------------------------------------------------------------------- +// Drag-and-drop on the map +// --------------------------------------------------------------------------- + +/** + * Set up the map container as a drop zone for .shp/.zip, .geojson/.json, .kml + * files. Dragging files over the map shows a visual overlay; dropping them + * routes to the correct import handler. + */ +function initMapDropZone() { + const container = document.querySelector('.map-container'); + if (!container) return; + + let dragCounter = 0; // track nested enter/leave events + + container.addEventListener('dragenter', (e) => { + e.preventDefault(); + dragCounter++; + container.classList.add('drag-over'); + }); + + container.addEventListener('dragover', (e) => { + e.preventDefault(); // required to allow drop + }); + + container.addEventListener('dragleave', (e) => { + e.preventDefault(); + dragCounter--; + if (dragCounter <= 0) { + dragCounter = 0; + container.classList.remove('drag-over'); + } + }); + + container.addEventListener('drop', (e) => { + e.preventDefault(); + dragCounter = 0; + container.classList.remove('drag-over'); + + const files = e.dataTransfer?.files; + if (!files || files.length === 0) return; + + // Build extension lookup to decide which handler to use + const byExt = indexFilesByExtension(files); + const exts = Object.keys(byExt); + + if (byExt.zip || byExt.shp) { + // Shapefile import (zip or loose .shp + companions) + const fakeEvt = { target: { files, value: '' } }; + Object.defineProperty(fakeEvt.target, 'value', { writable: true }); + handleShapefileImport(fakeEvt); + } else if (byExt.geojson || byExt.json) { + const file = byExt.geojson || byExt.json; + const fakeEvt = { target: { files: [file], value: '' } }; + Object.defineProperty(fakeEvt.target, 'value', { writable: true }); + handleGeoJSONImport(fakeEvt); + } else if (byExt.kml) { + const fakeEvt = { target: { files: [byExt.kml], value: '' } }; + Object.defineProperty(fakeEvt.target, 'value', { writable: true }); + handleKMLImport(fakeEvt); + } else { + showFileImportError( + 'Unsupported file type(s): ' + exts.map(e => '.' + e).join(', ') + + '. Drop .zip, .shp, .geojson, .json, or .kml files.' + ); + } + }); + + console.log('[FileImport] Map drop zone initialised'); +} + // ============================================================================ // Utilities // ============================================================================ @@ -1543,36 +2404,608 @@ function escapeHtml(text) { return div.innerHTML; } +// ============================================================================ +// Message Log — persistent stack in the right panel +// ============================================================================ + +const MESSAGE_LOG_MAX = 50; + +const MSG_CONFIG = { + error: { icon: 'bi-x-circle-fill', color: 'var(--destructive, #dc3545)' }, + warning: { icon: 'bi-exclamation-triangle-fill', color: 'var(--warning, #ffc107)' }, + success: { icon: 'bi-check-circle-fill', color: 'var(--success, #198754)' }, + info: { icon: 'bi-info-circle-fill', color: 'var(--primary, #0d6efd)' }, +}; + +/** + * Append a message to the persistent log in the right panel. + * Also logs to the browser console. + * + * @param {'error'|'warning'|'success'|'info'} type + * @param {string} text + */ +function logMessage(type, text) { + const cfg = MSG_CONFIG[type] || MSG_CONFIG.info; + + // Console mirror + const consoleFn = type === 'error' ? console.error + : type === 'warning' ? console.warn + : console.log; + consoleFn('[App]', text); + + const log = document.getElementById('message-log'); + if (!log) return; + + // Remove the "No messages yet" placeholder if present + const placeholder = log.querySelector('.text-muted'); + if (placeholder) placeholder.remove(); + + // Build the entry + const entry = document.createElement('div'); + entry.className = 'list-group-item message-log-entry py-2 px-3'; + const now = new Date(); + const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + entry.innerHTML = + `
    ` + + `` + + `
    ${escapeHtml(text)}
    ` + + `${time}` + + `
    `; + + // Prepend (newest first) + log.prepend(entry); + + // Cap the list + while (log.children.length > MESSAGE_LOG_MAX) { + log.lastElementChild.remove(); + } +} + +/** Wire up the "clear" button */ +function initMessageLog() { + const btn = document.getElementById('clear-message-log'); + if (btn) { + btn.addEventListener('click', () => { + const log = document.getElementById('message-log'); + if (log) { + log.innerHTML = '
    No messages yet.
    '; + } + }); + } +} + +// ============================================================================ +// Toast-style alerts (auto-dismiss) + persistent log +// ============================================================================ + function showError(message) { + logMessage('error', message); const el = document.getElementById('error-message'); if (el) { el.querySelector('.message-text').textContent = message; el.classList.remove('d-none'); - - // Auto-hide after 5 seconds - setTimeout(() => { - el.classList.add('d-none'); - }, 5000); - } else { - console.error(message); + setTimeout(() => el.classList.add('d-none'), 5000); } } function showSuccess(message) { + logMessage('success', message); const el = document.getElementById('success-message'); if (el) { el.querySelector('.message-text').textContent = message; el.classList.remove('d-none'); - - // Auto-hide after 3 seconds - setTimeout(() => { - el.classList.add('d-none'); - }, 3000); - } else { - console.log(message); + setTimeout(() => el.classList.add('d-none'), 3000); } } +function showWarning(message) { + logMessage('warning', message); + const el = document.getElementById('warning-message'); + if (el) { + el.querySelector('.message-text').textContent = message; + el.classList.remove('d-none'); + setTimeout(() => el.classList.add('d-none'), 5000); + } +} + +// ============================================================================ +// Fieldwork Mode +// ============================================================================ + +function initFieldworkMode() { + const toggle = document.getElementById('fieldwork-mode-toggle'); + if (!toggle) return; + + // Restore saved preference + const saved = localStorage.getItem('fieldwork-mode'); + if (saved === 'true') { + document.documentElement.classList.add('fieldwork-mode'); + toggle.checked = true; + } + + toggle.addEventListener('change', () => { + document.documentElement.classList.toggle('fieldwork-mode', toggle.checked); + localStorage.setItem('fieldwork-mode', toggle.checked); + console.log('[Settings] Fieldwork mode', toggle.checked ? 'ON' : 'OFF'); + }); +} + +// ============================================================================ +// Dark Mode +// ============================================================================ + +function initDarkMode() { + const toggle = document.getElementById('dark-mode-toggle'); + if (!toggle) return; + + function applyDark(on) { + document.documentElement.classList.toggle('dark-mode', on); + // Bootstrap 5.3 built-in dark mode support + document.documentElement.setAttribute('data-bs-theme', on ? 'dark' : 'light'); + } + + // Restore saved preference + const saved = localStorage.getItem('dark-mode'); + if (saved === 'true') { + toggle.checked = true; + applyDark(true); + } + + toggle.addEventListener('change', () => { + applyDark(toggle.checked); + localStorage.setItem('dark-mode', toggle.checked); + console.log('[Settings] Dark mode', toggle.checked ? 'ON' : 'OFF'); + }); +} + +// ============================================================================ +// Measurement System +// ============================================================================ + +function initMeasurementSystem() { + const toggle = document.getElementById('measurement-system-toggle'); + const label = document.getElementById('measurement-system-label'); + if (!toggle) return; + + function updateLabel() { + if (label) label.textContent = toggle.checked ? 'Imperial' : 'Metric'; + } + + // Restore saved preference + const saved = localStorage.getItem('measurement-system'); + if (saved === 'imperial') { + toggle.checked = true; + } + updateLabel(); + + // Apply saved setting to the scale bar on load + mapView?.setScaleBarUnits(saved || 'metric'); + + toggle.addEventListener('change', () => { + const system = toggle.checked ? 'imperial' : 'metric'; + localStorage.setItem('measurement-system', system); + updateLabel(); + mapView?.setScaleBarUnits(system); + console.log('[Settings] Measurement system:', system); + }); +} + +/** + * Default base map selector — persisted in localStorage. + * Keys must match those handled by MapView.setBaseMap(). + */ +function initDefaultBasemap() { + const select = document.getElementById('default-basemap-select'); + if (!select) return; + + // Restore saved preference (default: topo) + const saved = localStorage.getItem('default-basemap') || 'topo'; + select.value = saved; + + select.addEventListener('change', () => { + const key = select.value; + localStorage.setItem('default-basemap', key); + mapView?.setBaseMap(key); + console.log('[Settings] Default base map:', key); + }); +} + +/** + * Offline Map Tiles card — shows per-provider cache stats and offers a + * "Clear cached tiles" button. Stats refresh whenever the Settings panel + * is opened so the numbers are always current. + */ +function initOfflineTileCache() { + const statsEl = document.getElementById('tile-cache-stats'); + const clearBtn = document.getElementById('clear-tiles-btn'); + const offcanvas = document.getElementById('offcanvasBottom'); + if (!statsEl || !clearBtn || !offcanvas) return; + + /** Format a byte count into a human-friendly string. */ + function fmtBytes(bytes) { + if (!bytes) return '0 KB'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB'; + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; + } + + // Track in-flight refresh so rapid calls don't overlap and to allow a + // controllerchange handler to know when a refresh is already underway. + let refreshInFlight = null; + + /** Render the stats panel. */ + async function refresh() { + if (refreshInFlight) return refreshInFlight; + + // If the SW hasn't taken control yet, give the user a friendly hint + // instead of immediately failing. The wait inside getTileCacheStats() + // will resolve once the SW becomes available, at which point this + // refresh completes normally — no reload needed. + const swActive = !!navigator.serviceWorker?.controller; + statsEl.innerHTML = swActive + ? '
    Loading…
    ' + : '
    Initialising service worker…
    '; + + refreshInFlight = (async () => { + try { + const stats = await getTileCacheStats(); + + if (!stats) { + statsEl.innerHTML = ` +
    + Tile cache stats unavailable. Try reloading the page if this persists. +
    `; + return; + } + + const total = stats.totals; + const rows = stats.byProvider + .filter((p) => p.count > 0) + .map((p) => ` + + + + + `).join(''); + + let storageNote = ''; + const est = await getStorageEstimate(); + if (est && est.quota > 0) { + const pct = ((est.usage / est.quota) * 100).toFixed(1); + storageNote = ` +
    + Total app storage: ${fmtBytes(est.usage)} of ${fmtBytes(est.quota)} available (${pct}%) +
    `; + } + + if (total.count === 0) { + statsEl.innerHTML = ` +
    + No tiles cached yet. Pan and zoom the map to start caching tiles automatically. +
    ${storageNote}`; + clearBtn.disabled = true; + return; + } + + statsEl.innerHTML = ` +
    + ${total.count.toLocaleString()} tiles cached, ~${fmtBytes(total.estBytes)} on this device +
    +
    TableRecordsRecords
    - ${escapeHtml(t.name)} - ${t.count}
    + ${escapeHtml(t.name)} + ${t.count}${clearBtn}
    Failed to load
    Failed to load
    ${p.label}${p.count.toLocaleString()} / ${p.limit.toLocaleString()}${fmtBytes(p.estBytes)}
    + + + + + + ${rows} +
    Base mapCached / limitApprox. size
    ${storageNote}`; + clearBtn.disabled = false; + } finally { + refreshInFlight = null; + } + })(); + + return refreshInFlight; + } + + // Clear button — confirm, then clear, then refresh + clearBtn.addEventListener('click', async () => { + if (!confirm('Clear all cached map tiles from this device? You will need to be online to view them again.')) { + return; + } + clearBtn.disabled = true; + const ok = await clearTileCaches(); + if (ok) { + console.log('[Settings] Tile caches cleared'); + } else { + console.warn('[Settings] Tile-cache clear failed'); + } + await refresh(); + }); + + // Refresh stats whenever the Settings offcanvas opens + offcanvas.addEventListener('show.bs.offcanvas', refresh); + + // Auto-refresh when a (new) service worker takes control of the page — + // makes the panel populate as soon as the SW is available, even if the + // user is staring at it during initial install or during an SW update. + onServiceWorkerControllerChange(() => { + console.log('[Settings] SW controller changed → refreshing tile-cache stats'); + refresh(); + }); + + // Also do an initial render so the card isn't empty if Settings is open + // immediately on load. + refresh(); +} + +/** + * Offline-download dialog (Phase 2). Allows users to pre-fetch tiles for a + * chosen extent and zoom range so they can use the map without connectivity. + */ +function initOfflineDownloadDialog() { + const triggerBtn = document.getElementById('download-tiles-btn'); + const modalEl = document.getElementById('offline-download-modal'); + if (!triggerBtn || !modalEl) return; + + const modal = Modal.getOrCreateInstance(modalEl); + + // ----- Element refs ----- + const formView = document.getElementById('offline-download-form-view'); + const progressView = document.getElementById('offline-download-progress-view'); + const doneView = document.getElementById('offline-download-done-view'); + const cancelBtn = document.getElementById('offline-download-cancel-btn'); + const startBtn = document.getElementById('offline-download-start-btn'); + const closeDoneBtn = document.getElementById('offline-download-close-done-btn'); + const headerCloseBtn = document.getElementById('offline-download-close-btn'); + + const basemapSelect = document.getElementById('offline-basemap-select'); + const minZoomInput = document.getElementById('offline-min-zoom'); + const maxZoomInput = document.getElementById('offline-max-zoom'); + const ackCheck = document.getElementById('offline-ack-check'); + const estimateEl = document.getElementById('offline-estimate-detail'); + const estimateBox = document.getElementById('offline-estimate'); + + const areaViewRadio = document.getElementById('offline-area-view'); + const areaDistrictRadio = document.getElementById('offline-area-district'); + const areaGhanaRadio = document.getElementById('offline-area-ghana'); + const areaViewInfo = document.getElementById('offline-area-view-info'); + const areaDistrictInfo = document.getElementById('offline-area-district-info'); + + const progressBar = document.getElementById('offline-progress-bar'); + const progressPercent = document.getElementById('offline-progress-percent'); + const progressCounts = document.getElementById('offline-progress-counts'); + const progressOk = document.getElementById('offline-progress-ok'); + const progressFailed = document.getElementById('offline-progress-failed'); + const progressEta = document.getElementById('offline-progress-eta'); + + const doneTitle = document.getElementById('offline-done-title'); + const doneDetail = document.getElementById('offline-done-detail'); + + // ----- State ----- + let currentDownloader = null; + + /** Format byte count for display. */ + function fmtBytes(b) { + if (!b) return '0 KB'; + if (b < 1024 * 1024) return (b / 1024).toFixed(0) + ' KB'; + if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + ' MB'; + return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; + } + + /** Format ms → human-readable duration. */ + function fmtDuration(ms) { + if (!ms || ms < 1000) return '< 1 s'; + const s = Math.round(ms / 1000); + if (s < 60) return s + ' s'; + const m = Math.floor(s / 60); + const r = s % 60; + if (m < 60) return `${m} min ${r} s`; + const h = Math.floor(m / 60); + return `${h} h ${m % 60} min`; + } + + /** Get the chosen extent based on the radio selection. Returns null if invalid. */ + function getSelectedExtent() { + if (areaViewRadio.checked) { + return mapView?.getCurrentViewExtent() || null; + } + if (areaDistrictRadio.checked) { + return mapView?.getDistrictBoundaryExtent()?.extent || null; + } + if (areaGhanaRadio.checked) { + return GHANA_EXTENT_3857; + } + return null; + } + + /** Recalculate and update the live estimate display. */ + function updateEstimate() { + const baseMap = basemapSelect.value; + const minZ = parseInt(minZoomInput.value, 10); + const maxZ = parseInt(maxZoomInput.value, 10); + + if (Number.isNaN(minZ) || Number.isNaN(maxZ) || minZ > maxZ) { + estimateEl.textContent = 'Invalid zoom range'; + estimateBox.classList.replace('alert-info', 'alert-warning'); + startBtn.disabled = true; + return; + } + + const extent = getSelectedExtent(); + if (!extent) { + estimateEl.textContent = 'Selected area is not available.'; + estimateBox.classList.replace('alert-info', 'alert-warning'); + startBtn.disabled = true; + return; + } + + const tplMaxZoom = BASEMAP_TEMPLATES[baseMap]?.maxZoom ?? 19; + const effMaxZ = Math.min(maxZ, tplMaxZoom); + const count = countTiles(extent, minZ, effMaxZ); + const bytes = estimatedSizeBytes(count); + + let warningHTML = ''; + if (effMaxZ < maxZ) { + warningHTML = `
    Zoom ${maxZ} is above this provider's max (${tplMaxZoom}); will clamp to ${tplMaxZoom}.`; + } + if (count > 8000) { + warningHTML += `
    More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.`; + } + + estimateEl.innerHTML = + `${count.toLocaleString()} tiles · ` + + `~${fmtBytes(bytes)}` + + warningHTML; + + estimateBox.classList.toggle('alert-warning', !!warningHTML); + estimateBox.classList.toggle('alert-info', !warningHTML); + + startBtn.disabled = !ackCheck.checked || count === 0; + } + + /** Update the area-radio info labels (tile count + size estimate). */ + function updateAreaInfos() { + const view = mapView?.getCurrentViewExtent(); + if (view) { + areaViewInfo.textContent = ' · ready'; + } else { + areaViewInfo.textContent = ''; + } + + const dist = mapView?.getDistrictBoundaryExtent(); + if (dist) { + areaDistrictInfo.textContent = ''; + areaDistrictRadio.disabled = false; + } else { + areaDistrictInfo.textContent = ' (not loaded — connect online to fetch)'; + areaDistrictRadio.disabled = true; + if (areaDistrictRadio.checked) areaViewRadio.checked = true; + } + } + + /** Reset the modal to its initial form state. */ + function resetModal() { + formView.classList.remove('d-none'); + progressView.classList.add('d-none'); + doneView.classList.add('d-none'); + + startBtn.classList.remove('d-none'); + cancelBtn.classList.remove('d-none'); + cancelBtn.textContent = 'Cancel'; + closeDoneBtn.classList.add('d-none'); + headerCloseBtn.disabled = false; + + ackCheck.checked = false; + startBtn.disabled = true; + + currentDownloader = null; + } + + // ----- Event wiring ----- + + triggerBtn.addEventListener('click', () => { + resetModal(); + updateAreaInfos(); + updateEstimate(); + modal.show(); + }); + + // Recalculate estimate on any input change + basemapSelect.addEventListener('change', updateEstimate); + minZoomInput.addEventListener('input', updateEstimate); + maxZoomInput.addEventListener('input', updateEstimate); + areaViewRadio.addEventListener('change', updateEstimate); + areaDistrictRadio.addEventListener('change', updateEstimate); + areaGhanaRadio.addEventListener('change', updateEstimate); + ackCheck.addEventListener('change', updateEstimate); + + // Start the download + startBtn.addEventListener('click', async () => { + const baseMap = basemapSelect.value; + const minZ = parseInt(minZoomInput.value, 10); + const maxZ = parseInt(maxZoomInput.value, 10); + const extent = getSelectedExtent(); + if (!extent) return; + + // Switch UI to progress view + formView.classList.add('d-none'); + progressView.classList.remove('d-none'); + startBtn.classList.add('d-none'); + cancelBtn.textContent = 'Cancel download'; + headerCloseBtn.disabled = true; + + progressBar.style.width = '0%'; + progressBar.setAttribute('aria-valuenow', '0'); + progressPercent.textContent = '0%'; + progressCounts.textContent = '0 of 0 tiles'; + progressOk.textContent = '0'; + progressFailed.textContent = '0'; + progressEta.textContent = '—'; + + currentDownloader = new OfflineTileDownloader({ + baseMap, + extent3857: extent, + minZoom: minZ, + maxZoom: maxZ, + onProgress: (s) => { + if (s.total > 0) { + const pct = Math.min(100, Math.round((s.done / s.total) * 100)); + progressBar.style.width = pct + '%'; + progressBar.setAttribute('aria-valuenow', String(pct)); + progressPercent.textContent = pct + '%'; + progressCounts.textContent = `${s.done.toLocaleString()} of ${s.total.toLocaleString()} tiles`; + } + progressOk.textContent = s.ok.toLocaleString(); + progressFailed.textContent = s.failed.toLocaleString(); + progressEta.textContent = s.etaMs != null ? fmtDuration(s.etaMs) : '—'; + }, + }); + + let result; + try { + result = await currentDownloader.start(); + } catch (err) { + console.error('[OfflineDownload] failed:', err); + result = { phase: 'error', done: 0, total: 0, ok: 0, failed: 0 }; + } + + // Switch UI to done view + progressView.classList.add('d-none'); + doneView.classList.remove('d-none'); + cancelBtn.classList.add('d-none'); + closeDoneBtn.classList.remove('d-none'); + headerCloseBtn.disabled = false; + + if (result.phase === 'cancelled') { + doneTitle.textContent = 'Download cancelled'; + doneDetail.innerHTML = `Stopped after ${result.done.toLocaleString()} of ${result.total.toLocaleString()} tiles.
    ` + + `${result.ok.toLocaleString()} fetched · ${result.failed.toLocaleString()} failed.`; + } else if (result.phase === 'error') { + doneTitle.textContent = 'Download failed'; + doneDetail.textContent = 'See console for details.'; + } else { + doneTitle.textContent = 'Download complete'; + doneDetail.innerHTML = `${result.ok.toLocaleString()} tiles cached` + + (result.failed > 0 ? `, ${result.failed.toLocaleString()} failed` : '') + + `.
    Took ${fmtDuration(result.elapsedMs)}.`; + } + }); + + // Cancel button — either close modal (form view) or cancel download (progress view) + cancelBtn.addEventListener('click', () => { + if (currentDownloader) { + currentDownloader.cancel(); + } + }); + + // When modal is fully hidden, reset for next time + modalEl.addEventListener('hidden.bs.modal', () => { + if (currentDownloader) currentDownloader.cancel(); + resetModal(); + }); +} + // ============================================================================ // Start Application // ============================================================================ diff --git a/package-lock.json b/package-lock.json index 65d3a0d..dfeabcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { - "name": "lupmis-pwa", + "name": "lupmis2-pwa", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "lupmis-pwa", + "name": "lupmis2-pwa", "version": "1.0.0", "dependencies": { "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "ol": "^10.3.0", "ol-ext": "^4.0.24", + "shpjs": "^6.2.0", "sqlocal": "^0.16.0" }, "devDependencies": { @@ -22,6 +25,15 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -795,12 +807,32 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/rbush": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -813,6 +845,16 @@ "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==", "license": "ISC" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/bootstrap": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", @@ -848,6 +890,32 @@ ], "license": "MIT" }, + "node_modules/but-unzip": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/but-unzip/-/but-unzip-0.1.10.tgz", + "integrity": "sha512-hLfQ9WlUimmv/okzsRl6AYG3Ew5HNWhWgUslSR93FsDdeL0MAoQvmC/BJfs35lqEAO5t/QD7Y4vCFcPJtijt3A==", + "license": "Apache-2.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/coincident": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", @@ -863,6 +931,38 @@ "ws": "^8.16.0" } }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/earcut": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", @@ -911,6 +1011,17 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -929,6 +1040,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -968,12 +1085,64 @@ "node": ">=10.19" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, "node_modules/lerc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", "license": "Apache-2.0" }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1031,6 +1200,12 @@ "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", "license": "MIT" }, + "node_modules/parsedbf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-2.0.0.tgz", + "integrity": "sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==", + "license": "MIT" + }, "node_modules/pbf": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", @@ -1043,6 +1218,13 @@ "pbf": "bin/pbf" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1092,6 +1274,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proj4": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.3.tgz", + "integrity": "sha512-uKJXnf/RkHhExxnWHqQqy2J1bPc5Qo8XSGzrMSJTdPWUQDo1DkunIRBfAS0crQaP9bZCSKNjqYJdYWVov0hDXw==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", @@ -1122,6 +1317,16 @@ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/rbush": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", @@ -1131,6 +1336,13 @@ "quickselect": "^3.0.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -1140,6 +1352,16 @@ "protocol-buffers-schema": "^3.3.1" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rollup": { "version": "4.55.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", @@ -1185,6 +1407,17 @@ "fsevents": "~2.3.2" } }, + "node_modules/shpjs": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.2.0.tgz", + "integrity": "sha512-8cR/RKYHQepmVyBMtzZQ+1bnSbWrtLXS6aoEJmpUlOSHtSUddterebVxYmIWq2g9kOEX9jm2kjHiikyPX7cNQA==", + "license": "MIT", + "dependencies": { + "but-unzip": "^0.1.4", + "parsedbf": "^2.0.0", + "proj4": "^2.1.4" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1237,6 +1470,36 @@ } } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1254,6 +1517,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -1335,6 +1608,12 @@ "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", "license": "Apache-2.0" }, + "node_modules/wkt-parser": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.3.tgz", + "integrity": "sha512-myla+RrMj+WTlnHc8Y4HEwjBcBF9dqJ3vjff/zmlrn9V3OKOM1mZVIyNjlPEmOM9Jjr/PPut0tnaTs9NyHcK8Q==", + "license": "MIT" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", diff --git a/package.json b/package.json index d660e23..b4e6825 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "preview": "vite preview" }, "dependencies": { + "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", - "@popperjs/core": "^2.11.8", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "ol": "^10.3.0", "ol-ext": "^4.0.24", + "shpjs": "^6.2.0", "sqlocal": "^0.16.0" }, "devDependencies": { diff --git a/public/fonts/bebas-neue-latin-ext.woff2 b/public/fonts/bebas-neue-latin-ext.woff2 new file mode 100644 index 0000000..d8460fe Binary files /dev/null and b/public/fonts/bebas-neue-latin-ext.woff2 differ diff --git a/public/fonts/bebas-neue-latin.woff2 b/public/fonts/bebas-neue-latin.woff2 new file mode 100644 index 0000000..c3673e5 Binary files /dev/null and b/public/fonts/bebas-neue-latin.woff2 differ diff --git a/public/fonts/exo-latin-ext.woff2 b/public/fonts/exo-latin-ext.woff2 new file mode 100644 index 0000000..521748b Binary files /dev/null and b/public/fonts/exo-latin-ext.woff2 differ diff --git a/public/fonts/exo-latin.woff2 b/public/fonts/exo-latin.woff2 new file mode 100644 index 0000000..69df029 Binary files /dev/null and b/public/fonts/exo-latin.woff2 differ diff --git a/public/fonts/exo-vietnamese.woff2 b/public/fonts/exo-vietnamese.woff2 new file mode 100644 index 0000000..bb56eda Binary files /dev/null and b/public/fonts/exo-vietnamese.woff2 differ diff --git a/public/icons/README.txt b/public/icons/README.txt deleted file mode 100644 index 09fd858..0000000 --- a/public/icons/README.txt +++ /dev/null @@ -1 +0,0 @@ -Place PWA icons here (icon-72.png, icon-96.png, icon-128.png, icon-144.png, icon-152.png, icon-192.png, icon-384.png, icon-512.png) diff --git a/public/icons/luspa-128x128.png b/public/icons/luspa-128x128.png new file mode 100644 index 0000000..2a7afad Binary files /dev/null and b/public/icons/luspa-128x128.png differ diff --git a/public/icons/luspa-144x144.png b/public/icons/luspa-144x144.png new file mode 100644 index 0000000..96cc6e6 Binary files /dev/null and b/public/icons/luspa-144x144.png differ diff --git a/public/icons/luspa-152x152.png b/public/icons/luspa-152x152.png new file mode 100644 index 0000000..87b9777 Binary files /dev/null and b/public/icons/luspa-152x152.png differ diff --git a/public/icons/luspa-384x384.png b/public/icons/luspa-384x384.png new file mode 100644 index 0000000..f7f70be Binary files /dev/null and b/public/icons/luspa-384x384.png differ diff --git a/public/icons/luspa-72x72.png b/public/icons/luspa-72x72.png new file mode 100644 index 0000000..af6cde9 Binary files /dev/null and b/public/icons/luspa-72x72.png differ diff --git a/public/icons/luspa-96x96.png b/public/icons/luspa-96x96.png new file mode 100644 index 0000000..c5c7d2e Binary files /dev/null and b/public/icons/luspa-96x96.png differ diff --git a/public/icons/luspa-pdf.jpg b/public/icons/luspa-pdf.jpg new file mode 100644 index 0000000..6c9dc01 Binary files /dev/null and b/public/icons/luspa-pdf.jpg differ diff --git a/public/icons/luspa.icon/Assets/luspalogo.png b/public/icons/luspa.icon/Assets/luspalogo.png deleted file mode 100644 index 1000e28..0000000 Binary files a/public/icons/luspa.icon/Assets/luspalogo.png and /dev/null differ diff --git a/public/icons/luspa.icon/icon.json b/public/icons/luspa.icon/icon.json deleted file mode 100644 index 4295310..0000000 --- a/public/icons/luspa.icon/icon.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "fill" : { - "linear-gradient" : [ - "display-p3:0.10199,0.05884,0.32544,1.00000", - "display-p3:0.27051,0.49023,0.74121,1.00000" - ], - "orientation" : { - "start" : { - "x" : 0.5, - "y" : 0 - }, - "stop" : { - "x" : 0.5, - "y" : 0.7 - } - } - }, - "groups" : [ - { - "layers" : [ - { - "image-name" : "luspalogo.png", - "name" : "luspalogo", - "position" : { - "scale" : 1.8, - "translation-in-points" : [ - 0, - 0 - ] - } - } - ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "translucency" : { - "enabled" : true, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index d8ab69a..7ae399c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,58 +1,58 @@ { "name": "LUPMIS2 Drawing Tools", - "short_name": "LUPMIS", + "short_name": "LUPMIS2", "description": "Map and GIS functions for Land Use Planning in Ghana", - "start_url": "/", - "scope": "/", + "start_url": "./", + "scope": "./", "display": "standalone", "background_color": "#ffffff", "theme_color": "#005eb8", "orientation": "any", "icons": [ { - "src": "/icons/icon-72.png", + "src": "./icons/luspa-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-96.png", + "src": "./icons/luspa-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-128.png", + "src": "./icons/luspa-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-144.png", + "src": "./icons/luspa-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "any" }, { - "src": "/icons/icon-152.png", + "src": "./icons/luspa-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "any" }, { - "src": "/icons/luspa-192x192.png", + "src": "./icons/luspa-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icons/icon-384.png", + "src": "./icons/luspa-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "any" }, { - "src": "/icons/luspa-512x512.png", + "src": "./icons/luspa-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" diff --git a/public/sw.js b/public/sw.js index 257c9a9..4c981a5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,29 +1,81 @@ /** * Service Worker - * + * * Handles caching of: * - App shell (HTML, CSS, JS) - * - Map tiles (runtime caching) + * - Map tiles (passive runtime caching, per-host buckets) * - API responses (network-first) - * + * * Note: Database operations are handled by the SharedWorker (shared-db-worker.js), * NOT by this service worker. They serve different purposes: * - Service Worker: Caching, offline asset serving, push notifications * - SharedWorker: Shared database connection across tabs */ -const CACHE_VERSION = 'v1'; -const SHELL_CACHE = `shell-${CACHE_VERSION}`; -const TILES_CACHE = `tiles-${CACHE_VERSION}`; +// v3: lower per-cache limits (5000 → 1500) and counter-based eviction to +// prevent Safari memory-pressure reloads. +// v4: raise OSM and Topographic limits to 8000 to support active offline +// downloads (Phase 2). Other providers stay at 1500. +const CACHE_VERSION = 'v4'; +const SHELL_CACHE = `shell-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`; -const API_CACHE = `api-${CACHE_VERSION}`; +const API_CACHE = `api-${CACHE_VERSION}`; -// Maximum number of tiles to cache -const MAX_TILES = 500; +// ---------------------------------------------------------------------------- +// Tile caches — one per provider so users can clear them independently. +// Limits are per-cache (not global). 5 000 tiles ≈ ~150 MB at ~30 KB/tile, +// which covers a Ghana district at zoom 10–15 (typical field-work range). +// ---------------------------------------------------------------------------- +const TILES_OSM = `tiles-osm-${CACHE_VERSION}`; +const TILES_TOPO = `tiles-topo-${CACHE_VERSION}`; +const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`; +const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`; +const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`; -// App shell assets - precached on install -// Vite will generate hashed filenames, so we cache the entry points -// and let the browser handle the hashed assets +// Per-provider tile limits. +// • OSM and Topographic are the providers offered for active offline +// download (Phase 2 dialog), so they get a higher cap (~240 MB each at +// ~30 KB/tile) — enough for a typical Ghana district at zoom 10–15. +// • The other providers serve passive caching only (whatever the user has +// already viewed), so 1 500 tiles ≈ 45 MB is plenty. +// +// Total max ≈ 5 × ~150 MB = ~750 MB on disk in the worst case, but only the +// two downloadable buckets are likely to fill. Eviction sweeps run every 100 +// inserts (see EVICTION_CHECK_INTERVAL) so memory pressure stays bounded. +const TILE_LIMITS = { + [TILES_OSM]: 8000, + [TILES_TOPO]: 8000, + [TILES_SATELLITE]: 1500, + [TILES_CARTO_LIGHT]: 1500, + [TILES_CARTO_DARK]: 1500, +}; + +// Per-cache running insert counter, in memory. Avoids calling cache.keys() +// (which materialises every Request object in the cache) on every put — that +// was the cause of the Safari "reloaded due to memory pressure" failures. +// +// We only run a real eviction sweep every EVICTION_CHECK_INTERVAL inserts. +const _tileInsertCounters = new Map(); // cacheName → number of inserts since last eviction +const EVICTION_CHECK_INTERVAL = 100; + +// Friendly name shown in the UI (matches Settings card labels) +const TILE_CACHE_LABELS = { + [TILES_OSM]: 'OpenStreetMap', + [TILES_TOPO]: 'Topographic', + [TILES_SATELLITE]: 'Satellite', + [TILES_CARTO_LIGHT]: 'Carto Light', + [TILES_CARTO_DARK]: 'Carto Dark', +}; + +const ALL_TILE_CACHES = Object.keys(TILE_LIMITS); + +// Approximate average tile size — used for storage estimation. +// Real measurements: PNG tiles range 5–80 KB; 30 KB is a good middle ground. +const AVG_TILE_BYTES = 30 * 1024; + +// ---------------------------------------------------------------------------- +// App shell assets — precached on install. +// ---------------------------------------------------------------------------- const SHELL_ASSETS = [ '/', '/index.html', @@ -37,7 +89,7 @@ const SHELL_ASSETS = [ self.addEventListener('install', (event) => { console.log('[SW] Installing...'); - + event.waitUntil( caches.open(SHELL_CACHE) .then((cache) => { @@ -54,18 +106,26 @@ self.addEventListener('install', (event) => { self.addEventListener('activate', (event) => { console.log('[SW] Activating...'); - + event.waitUntil( caches.keys() .then((cacheNames) => { + // Build the set of caches that should remain + const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]); + return Promise.all( cacheNames + // Delete anything that: + // • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-) + // • but is NOT in the current keep set + // This includes the legacy "tiles-v1" single bucket. .filter((name) => { - // Delete old version caches - return (name.startsWith('shell-') && name !== SHELL_CACHE) || - (name.startsWith('tiles-') && name !== TILES_CACHE) || - (name.startsWith('modules-') && name !== MODULES_CACHE) || - (name.startsWith('api-') && name !== API_CACHE); + const isOurs = + name.startsWith('shell-') || + name.startsWith('tiles-') || + name.startsWith('modules-') || + name.startsWith('api-'); + return isOurs && !keep.has(name); }) .map((name) => { console.log('[SW] Deleting old cache:', name); @@ -84,17 +144,28 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); - + // Only handle GET requests if (request.method !== 'GET') return; - + // Skip chrome-extension and other non-http(s) requests if (!url.protocol.startsWith('http')) return; - - // Route to appropriate caching strategy - if (isMapTile(url)) { - event.respondWith(cacheThenNetwork(request, TILES_CACHE, MAX_TILES)); - } else if (isApiRequest(url)) { + + // Skip worker files and Vite dev-server node_modules requests — + // intercepting these breaks module workers (e.g. SQLocal/SQLite). + if (url.pathname.includes('node_modules') || + url.search.includes('worker_file') || + request.destination === 'worker') return; + + // ----- TILE REQUESTS — passive cache-then-network (per-host bucket) ----- + const tileCache = getTileCacheName(url); + if (tileCache) { + event.respondWith(tileCacheThenNetwork(request, tileCache)); + return; + } + + // ----- OTHER ROUTES (unchanged) ----- + if (isApiRequest(url)) { event.respondWith(networkFirst(request, API_CACHE)); } else if (isModuleAsset(url)) { event.respondWith(staleWhileRevalidate(request, MODULES_CACHE)); @@ -108,15 +179,36 @@ self.addEventListener('fetch', (event) => { // URL CLASSIFICATION // ============================================================================ -function isMapTile(url) { - // Common tile server patterns for all our base maps - return url.hostname.includes('tile.openstreetmap.org') || - url.hostname.includes('opentopomap.org') || - url.hostname.includes('arcgisonline.com') || - url.hostname.includes('basemaps.cartocdn.com') || - url.hostname.includes('tiles.') || - url.pathname.match(/\/\d+\/\d+\/\d+\.(png|jpg|pbf)$/) || - url.pathname.match(/\/tile\/\d+\/\d+\/\d+/); +/** + * Classify a URL into the appropriate tile cache. + * Returns `null` for non-tile requests, or for tile providers we deliberately + * do NOT cache (e.g. Google — caching is forbidden by their ToS). + */ +function getTileCacheName(url) { + const host = url.hostname; + + // OpenStreetMap — tile.openstreetmap.org and a/b/c subdomains + if (host.endsWith('tile.openstreetmap.org')) return TILES_OSM; + + // OpenTopoMap — a/b/c.tile.opentopomap.org + if (host.endsWith('tile.opentopomap.org') || host.endsWith('opentopomap.org')) return TILES_TOPO; + + // Carto Basemaps — light_all / dark_all distinguished by path + if (host.endsWith('basemaps.cartocdn.com')) { + if (url.pathname.includes('/light_all/')) return TILES_CARTO_LIGHT; + if (url.pathname.includes('/dark_all/')) return TILES_CARTO_DARK; + return null; // unknown Carto style + } + + // Esri — server.arcgisonline.com + if (host.endsWith('arcgisonline.com')) return TILES_SATELLITE; + + // Google — caching forbidden by ToS, do not store + if (host.endsWith('google.com') || host.endsWith('googleapis.com')) return null; + + // Other tile providers (WMS endpoints, OWS, custom) — not cached at this layer + // (the user's "online only" toast handles those). + return null; } function isApiRequest(url) { @@ -129,7 +221,6 @@ function isModuleAsset(url) { } function isAppAsset(url) { - // Same origin, common asset extensions return url.origin === self.location.origin && (url.pathname.endsWith('.html') || url.pathname.endsWith('.css') || @@ -144,13 +235,13 @@ function isAppAsset(url) { // ============================================================================ /** - * Cache First - Use cache, fallback to network - * Best for: App shell, static assets + * Cache First — Use cache, fallback to network. + * Best for: App shell, static assets. */ async function cacheFirst(request, cacheName) { const cached = await caches.match(request); if (cached) return cached; - + try { const response = await fetch(request); if (response.ok) { @@ -159,7 +250,6 @@ async function cacheFirst(request, cacheName) { } return response; } catch (error) { - // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline.html'); } @@ -168,8 +258,8 @@ async function cacheFirst(request, cacheName) { } /** - * Network First - Try network, fallback to cache - * Best for: API requests, dynamic content + * Network First — Try network, fallback to cache. + * Best for: API requests, dynamic content. */ async function networkFirst(request, cacheName) { try { @@ -187,89 +277,141 @@ async function networkFirst(request, cacheName) { } /** - * Stale While Revalidate - Return cache immediately, update in background - * Best for: Module assets, frequently updated content + * Stale While Revalidate — Return cache immediately, update in background. + * Best for: Module assets, frequently updated content. */ async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); - + const fetchPromise = fetch(request).then((response) => { if (response.ok) { cache.put(request, response.clone()); } return response; }).catch(() => cached); - + return cached || fetchPromise; } /** - * Cache Then Network with limit - Cache tiles with size limit - * Best for: Map tiles + * Tile Cache then Network — Per-host bucket with size limit. + * Cache first; on miss, fetch from network and store. + * + * Memory-conservative eviction: + * • Increments an in-memory counter on every successful insert + * • Only calls cache.keys() (which materialises all Request objects) every + * EVICTION_CHECK_INTERVAL inserts — so the cost is amortised + * • Eviction drops the oldest 10 % when over the per-host limit + * + * On network failure (offline), serves a 408 so the map renders a blank tile + * rather than throwing. */ -async function cacheThenNetwork(request, cacheName, maxItems) { +async function tileCacheThenNetwork(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); - if (cached) return cached; - + try { const response = await fetch(request); - + if (response.ok) { - // Check cache size and trim if needed - const keys = await cache.keys(); - if (keys.length >= maxItems) { - // Remove oldest entries (first 10%) - const toDelete = keys.slice(0, Math.ceil(maxItems * 0.1)); - await Promise.all(toDelete.map(key => cache.delete(key))); + // Bump the counter; periodically run a real eviction sweep + const count = (_tileInsertCounters.get(cacheName) || 0) + 1; + _tileInsertCounters.set(cacheName, count); + + if (count % EVICTION_CHECK_INTERVAL === 0) { + // Reset the counter — next sweep is another EVICTION_CHECK_INTERVAL away + _tileInsertCounters.set(cacheName, 0); + await maybeEvict(cache, cacheName); } - - cache.put(request, response.clone()); + + // Don't await put() — it can run after we return the response, keeping + // the fetch hot path lightweight. + cache.put(request, response.clone()).catch((err) => { + // QuotaExceededError → run an immediate eviction sweep and retry once + if (err && err.name === 'QuotaExceededError') { + maybeEvict(cache, cacheName, /* force */ true).catch(() => {}); + } + }); } - + return response; } catch (error) { - // For tiles, just fail silently - map will show blank tile + // Offline — let the map renderer show a blank tile return new Response('', { status: 408, statusText: 'Offline' }); } } +/** + * Run an eviction sweep on a cache, dropping the oldest 10 % of entries + * when over the per-cache limit. Heavy: only call periodically. + */ +async function maybeEvict(cache, cacheName, force = false) { + try { + const limit = TILE_LIMITS[cacheName] || 1500; + const keys = await cache.keys(); + if (force || keys.length >= limit) { + const drop = Math.max(1, Math.ceil(limit * 0.1)); + const toDelete = keys.slice(0, drop); + await Promise.all(toDelete.map((k) => cache.delete(k))); + } + } catch (err) { + console.warn('[SW] eviction sweep failed for', cacheName, err); + } +} + // ============================================================================ // MESSAGE HANDLING // ============================================================================ self.addEventListener('message', (event) => { const { type, payload } = event.data || {}; - + switch (type) { case 'SKIP_WAITING': self.skipWaiting(); break; - + case 'CACHE_MODULES': cacheModules(payload.modules); break; - + case 'CLEAR_USER_CACHE': clearUserCaches(); break; - + case 'GET_CACHE_STATUS': - getCacheStatus().then(status => { + getCacheStatus().then((status) => { event.source.postMessage({ type: 'CACHE_STATUS', status }); }); break; + + // ----- Tile-cache management (Phase 1 offline maps) ----- + case 'GET_TILE_STATS': + getTileStats().then((stats) => { + event.source.postMessage({ type: 'TILE_STATS', stats }); + }); + break; + + case 'CLEAR_TILE_CACHES': + clearTileCaches().then(() => { + event.source.postMessage({ type: 'TILE_CACHES_CLEARED' }); + }); + break; } }); +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + /** - * Cache specific modules on demand + * Cache specific modules on demand. */ async function cacheModules(moduleNames) { const cache = await caches.open(MODULES_CACHE); - + for (const moduleName of moduleNames) { try { const moduleAssets = [ @@ -277,9 +419,8 @@ async function cacheModules(moduleNames) { `/modules/${moduleName}/index.css`, `/modules/${moduleName}/index.html` ]; - + await cache.addAll(moduleAssets.filter(async (url) => { - // Only cache assets that exist try { const response = await fetch(url, { method: 'HEAD' }); return response.ok; @@ -287,7 +428,7 @@ async function cacheModules(moduleNames) { return false; } })); - + console.log('[SW] Cached module:', moduleName); } catch (error) { console.warn('[SW] Failed to cache module:', moduleName, error); @@ -296,7 +437,8 @@ async function cacheModules(moduleNames) { } /** - * Clear user-specific caches (call on logout) + * Clear user-specific caches (call on logout). + * Tile caches are NOT cleared here — those belong to the device, not the user. */ async function clearUserCaches() { await caches.delete(API_CACHE); @@ -305,17 +447,90 @@ async function clearUserCaches() { } /** - * Get cache status information + * Get summary status of all caches (count of entries in each). */ async function getCacheStatus() { const cacheNames = await caches.keys(); const status = {}; - + for (const name of cacheNames) { const cache = await caches.open(name); const keys = await cache.keys(); status[name] = keys.length; } - + return status; } + +/** + * Get per-provider tile cache statistics. + * + * Returns shape: + * { + * totals: { count, estBytes }, + * byProvider: [{ key, label, count, limit, estBytes }, ...] + * } + * + * estBytes is an approximation (count × AVG_TILE_BYTES). For an exact size, + * the caller can use navigator.storage.estimate() on the page side. + * + * Result is cached for STATS_TTL_MS so rapid re-queries (e.g. multiple + * Settings opens) don't re-enumerate every cache. + */ +const STATS_TTL_MS = 10 * 1000; +let _cachedStats = null; +let _cachedStatsAt = 0; + +async function getTileStats({ force = false } = {}) { + const now = Date.now(); + if (!force && _cachedStats && (now - _cachedStatsAt) < STATS_TTL_MS) { + return _cachedStats; + } + + const byProvider = []; + let totalCount = 0; + + for (const cacheName of ALL_TILE_CACHES) { + let count = 0; + if (await caches.has(cacheName)) { + const cache = await caches.open(cacheName); + // matchAll returns a smaller payload than keys() on Safari, but neither + // is free. Done at most once per STATS_TTL_MS thanks to the cache above. + const keys = await cache.keys(); + count = keys.length; + } + byProvider.push({ + key: cacheName, + label: TILE_CACHE_LABELS[cacheName] || cacheName, + count, + limit: TILE_LIMITS[cacheName] || 0, + estBytes: count * AVG_TILE_BYTES, + }); + totalCount += count; + } + + _cachedStats = { + totals: { + count: totalCount, + estBytes: totalCount * AVG_TILE_BYTES, + }, + byProvider, + }; + _cachedStatsAt = now; + return _cachedStats; +} + +/** + * Delete every tile cache. Frees the device storage used by cached map tiles. + * Does not affect app-shell, modules, or API caches. + */ +async function clearTileCaches() { + const results = await Promise.all( + ALL_TILE_CACHES.map((name) => caches.delete(name)) + ); + // Reset counters and invalidate stats cache + _tileInsertCounters.clear(); + _cachedStats = null; + _cachedStatsAt = 0; + console.log('[SW] Cleared tile caches:', ALL_TILE_CACHES.filter((_, i) => results[i])); +} diff --git a/sql/create_landuse_parcels.sql b/sql/create_landuse_parcels.sql new file mode 100644 index 0000000..d044c78 --- /dev/null +++ b/sql/create_landuse_parcels.sql @@ -0,0 +1,207 @@ +-- ============================================================================ +-- LUPMIS — Land Use Parcels schema +-- ============================================================================ +-- Source: "LAND USE INFORMATION FOR LUPMIS" (LUSPA, February 2026, revised) +-- Implements the parcel-attribute table defined by Stephen / LUSPA, with a +-- PostGIS geometry column and the indices needed for typical access patterns +-- (spatial queries, lookup by zone / district / locality, time filtering). +-- +-- Conventions: +-- • Identifiers are unquoted (lowercase) — PostgreSQL folds them to lower +-- case anyway, and this avoids the need for double-quotes in queries. +-- • Source column names are PascalCase / Mixed_Case in the spec; their +-- mapping to snake_case is shown in COMMENT ON COLUMN. +-- • Geometry is stored in EPSG:4326 (WGS 84) for portability with the +-- remote API. The MultiPolygon type accommodates parcels with islands +-- or multi-part shapes. +-- +-- Run as a database superuser (or a role with CREATEEXTENSION privilege) +-- in the target database. +-- ============================================================================ + +-- PostGIS is required for the geometry column and spatial index. +CREATE EXTENSION IF NOT EXISTS postgis; + +-- Drop existing table for clean re-runs in dev. Comment out for production. +-- DROP TABLE IF EXISTS public.landuse_parcels CASCADE; + +-- --------------------------------------------------------------------------- +-- Table: public.landuse_parcels +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS public.landuse_parcels ( + id BIGSERIAL PRIMARY KEY, + + -- Spec field 1: UPN — Unique Parcel Number (Integer, up to 10 digits). + -- 10-digit integers can exceed INTEGER's max (2,147,483,647), hence BIGINT. + upn BIGINT NOT NULL, + + -- Spec field 2: Style — Colour Assign ID (Integer, 2 digits). + -- References the colour palette defined in the Revised Zoning Guidelines + -- and Planning Standards (2025). Optional FK to a lookup table. + style SMALLINT, + + -- Spec field 3: Landuse — Broad land use (Text, 50). + landuse VARCHAR(50), + + -- Spec field 4: Zone_Code — Zone acronym (Text, 5), e.g. "Re A". + zone_code VARCHAR(5), + + -- Spec field 5: Zone_Name — Zone name (Text, 50), e.g. "Residential Zone A". + zone_name VARCHAR(50), + + -- Spec field 6: Sector — Sector number of plan area (Text, 5). + sector VARCHAR(5), + + -- Spec field 7: Block — Block name within the sector (Text, 3). + block VARCHAR(3), + + -- Spec field 8: Parcel_No — Plot number for land registration (Text, 5). + parcel_no VARCHAR(5), + + -- Spec field 9: Prop_No — Property number for street addressing (Text, 5). + prop_no VARCHAR(5), + + -- Spec field 10: St_Name — Street name (Text, 18). From the Street Naming + -- and Property Addressing System (SNPAS). + st_name VARCHAR(18), + + -- Spec field 11: Prop_Add — Property address (Text, 25). + prop_add VARCHAR(25), + + -- Spec field 12: Fac_Name — Facility name (Text, 100). + fac_name VARCHAR(100), + + -- Spec field 13: Min_Height — Minimum building height in storeys (Integer, 3). + min_height SMALLINT, + + -- Spec field 14: Max_Height — Maximum building height in storeys (Integer, 3). + max_height SMALLINT, + + -- Spec field 15: Eff_Date — Effective approval date by the District SPC. + eff_date DATE, + + -- Spec field 16: LP_Name — Local plan name (Text, 100). + lp_name VARCHAR(100), + + -- Spec field 17: Locality — Community / area name (Text, 50). + locality VARCHAR(50), + + -- Spec field 18: MMDA — Metropolitan / Municipal / District Assembly + -- abbreviation (Text, 10), e.g. "LADMA". + mmda VARCHAR(10), + + -- Spec field 19: Last_Update — Last update on a parcel (e.g. change of + -- use approved by SPC). + last_update DATE, + + -- Spec field 20: Remarks — Additional info (Text, 200). + remarks VARCHAR(200), + + -- ------------------------------------------------------------------ + -- Geometry — parcel polygon in WGS 84 (EPSG:4326). + -- MultiPolygon allows parcels with islands or disjoint parts. + -- ------------------------------------------------------------------ + geom geometry(MultiPolygon, 4326), + + -- ------------------------------------------------------------------ + -- Audit columns (not in the spec, added for change tracking) + -- ------------------------------------------------------------------ + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- ------------------------------------------------------------------ + -- Constraints + -- ------------------------------------------------------------------ + CONSTRAINT uq_landuse_parcels_upn UNIQUE (upn), + CONSTRAINT ck_landuse_parcels_style CHECK (style IS NULL OR style >= 0), + CONSTRAINT ck_landuse_parcels_min_height CHECK (min_height IS NULL OR min_height >= 0), + CONSTRAINT ck_landuse_parcels_max_height CHECK (max_height IS NULL OR max_height >= 0), + CONSTRAINT ck_landuse_parcels_height_order + CHECK (min_height IS NULL OR max_height IS NULL OR min_height <= max_height) +); + +-- --------------------------------------------------------------------------- +-- Column comments — preserve the source-document descriptions +-- --------------------------------------------------------------------------- +COMMENT ON TABLE public.landuse_parcels IS 'Land use parcels — LUSPA spec, February 2026 (revised).'; + +COMMENT ON COLUMN public.landuse_parcels.upn IS 'UPN — Unique Parcel Number (Integer, 10 digits).'; +COMMENT ON COLUMN public.landuse_parcels.style IS 'Style — Colour Assign ID per Revised Zoning Guidelines (2025).'; +COMMENT ON COLUMN public.landuse_parcels.landuse IS 'Broad land use, e.g. Residential, Commercial, Mixed.'; +COMMENT ON COLUMN public.landuse_parcels.zone_code IS 'Zone code (acronym), e.g. Re A.'; +COMMENT ON COLUMN public.landuse_parcels.zone_name IS 'Zone name, e.g. Residential Zone A.'; +COMMENT ON COLUMN public.landuse_parcels.sector IS 'Sector number of the plan area.'; +COMMENT ON COLUMN public.landuse_parcels.block IS 'Block name within the sector.'; +COMMENT ON COLUMN public.landuse_parcels.parcel_no IS 'Plot number for land registration.'; +COMMENT ON COLUMN public.landuse_parcels.prop_no IS 'Property number for street addressing.'; +COMMENT ON COLUMN public.landuse_parcels.st_name IS 'Street name (max 18 characters, per SNPAS).'; +COMMENT ON COLUMN public.landuse_parcels.prop_add IS 'Property address of parcel.'; +COMMENT ON COLUMN public.landuse_parcels.fac_name IS 'Facility name of property.'; +COMMENT ON COLUMN public.landuse_parcels.min_height IS 'Minimum building height (storeys).'; +COMMENT ON COLUMN public.landuse_parcels.max_height IS 'Maximum building height (storeys).'; +COMMENT ON COLUMN public.landuse_parcels.eff_date IS 'Effective approval date by the District Spatial Planning Committee.'; +COMMENT ON COLUMN public.landuse_parcels.lp_name IS 'Local plan name.'; +COMMENT ON COLUMN public.landuse_parcels.locality IS 'Name of community or area.'; +COMMENT ON COLUMN public.landuse_parcels.mmda IS 'Metropolitan/Municipal/District Assembly abbreviation, e.g. LADMA.'; +COMMENT ON COLUMN public.landuse_parcels.last_update IS 'Last update on a parcel (e.g. change of use approved by SPC).'; +COMMENT ON COLUMN public.landuse_parcels.remarks IS 'Additional information on the parcel.'; +COMMENT ON COLUMN public.landuse_parcels.geom IS 'Parcel boundary geometry (MultiPolygon, EPSG:4326).'; +COMMENT ON COLUMN public.landuse_parcels.created_at IS 'Row-creation timestamp (audit).'; +COMMENT ON COLUMN public.landuse_parcels.updated_at IS 'Row last-modified timestamp (audit, maintained by trigger).'; + +-- --------------------------------------------------------------------------- +-- Indices +-- --------------------------------------------------------------------------- + +-- Spatial index — required for any ST_Intersects / ST_Within / map-bbox query. +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_geom + ON public.landuse_parcels + USING GIST (geom); + +-- B-tree indices for common attribute lookups. +-- (uq_landuse_parcels_upn already creates an implicit index on upn.) +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_zone_code + ON public.landuse_parcels (zone_code); + +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_mmda + ON public.landuse_parcels (mmda); + +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_locality + ON public.landuse_parcels (locality); + +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_lp_name + ON public.landuse_parcels (lp_name); + +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_eff_date + ON public.landuse_parcels (eff_date); + +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_last_update + ON public.landuse_parcels (last_update); + +-- Composite index for the very common "find all parcels in MMDA X with zone Y" query. +CREATE INDEX IF NOT EXISTS idx_landuse_parcels_mmda_zone + ON public.landuse_parcels (mmda, zone_code); + +-- --------------------------------------------------------------------------- +-- Trigger — keep updated_at fresh on every UPDATE +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION public.fn_landuse_parcels_set_updated_at() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS trg_landuse_parcels_set_updated_at ON public.landuse_parcels; + +CREATE TRIGGER trg_landuse_parcels_set_updated_at +BEFORE UPDATE ON public.landuse_parcels +FOR EACH ROW +EXECUTE FUNCTION public.fn_landuse_parcels_set_updated_at(); + +-- ============================================================================ +-- End of script +-- ============================================================================ diff --git a/src/components/MapTools.js b/src/components/MapTools.js index 774c6b4..ce335c4 100644 --- a/src/components/MapTools.js +++ b/src/components/MapTools.js @@ -18,6 +18,7 @@ import { LineString, Circle, Polygon } from 'ol/geom'; import { getLength, getArea } from 'ol/sphere'; import Feature from 'ol/Feature'; import { unByKey } from 'ol/Observable'; +import { formatLength, formatArea, formatCircleExtent } from '../units.js'; // ol-ext imports import EditBar from 'ol-ext/control/EditBar'; @@ -116,40 +117,6 @@ export class MapTools { }); } - /** - * Format length output - */ - formatLength(length) { - if (length > 1000) { - return (Math.round(length / 1000 * 100) / 100) + ' km'; - } else { - return (Math.round(length * 100) / 100) + ' m'; - } - } - - /** - * Format area output - */ - formatArea(area) { - if (area > 1000000) { - return (Math.round(area / 1000000 * 100) / 100) + ' km²'; - } else { - return (Math.round(area * 100) / 100) + ' m²'; - } - } - - /** - * Format circle extent (bounding box area) - */ - formatCircleExtent(radius) { - const area = Math.PI * radius * radius; - if (area > 1000000) { - return (Math.round(area / 1000000 * 100) / 100) + ' km²'; - } else { - return (Math.round(area * 100) / 100) + ' m²'; - } - } - /** * Create measurement tooltip overlay */ @@ -231,8 +198,8 @@ export class MapTools { if (geom instanceof Circle) { const radius = geom.getRadius(); - const area = this.formatCircleExtent(radius); - const radiusFormatted = this.formatLength(radius); + const area = formatCircleExtent(radius); + const radiusFormatted = formatLength(radius); const output = `${radiusFormatted}
    ${area}`; @@ -311,7 +278,7 @@ export class MapTools { listener = sketch.getGeometry().on('change', (e) => { const geom = e.target; const length = getLength(geom); - const output = this.formatLength(length); + const output = formatLength(length); this.measureTooltipElement.innerHTML = output; this.measureTooltip.setPosition(geom.getLastCoordinate()); @@ -364,7 +331,7 @@ export class MapTools { listener = sketch.getGeometry().on('change', (e) => { const geom = e.target; const area = getArea(geom); - const output = this.formatArea(area); + const output = formatArea(area); this.measureTooltipElement.innerHTML = output; this.measureTooltip.setPosition(geom.getInteriorPoint().getCoordinates()); @@ -376,6 +343,10 @@ export class MapTools { const geom = feature.getGeometry(); const area = getArea(geom); + // Tag so the double-click handler can identify it + feature.set('_layerType', 'measure_area'); + feature.set('_area', area); + this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static'; this.measureTooltipElement = null; this.createMeasureTooltip(); diff --git a/src/components/MapView.js b/src/components/MapView.js index 08b8716..5179a7f 100644 --- a/src/components/MapView.js +++ b/src/components/MapView.js @@ -20,9 +20,13 @@ import Map from 'ol/Map'; import View from 'ol/View'; import Overlay from 'ol/Overlay'; import TileLayer from 'ol/layer/Tile'; +import ImageLayer from 'ol/layer/Image'; import LayerGroup from 'ol/layer/Group'; import VectorLayer from 'ol/layer/Vector'; +import VectorImageLayer from 'ol/layer/VectorImage'; import VectorSource from 'ol/source/Vector'; +import ImageWMS from 'ol/source/ImageWMS'; +import TileWMS from 'ol/source/TileWMS'; import OSM from 'ol/source/OSM'; import XYZ from 'ol/source/XYZ'; import { fromLonLat, toLonLat } from 'ol/proj'; @@ -33,6 +37,7 @@ import GeoJSON from 'ol/format/GeoJSON'; import { getArea, getLength } from 'ol/sphere'; import { fromCircle } from 'ol/geom/Polygon'; import ScaleLine from 'ol/control/ScaleLine'; +import { formatLength, formatLengthFull, formatArea, formatAreaFull } from '../units.js'; // ol-ext LayerSwitcher import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; @@ -57,10 +62,30 @@ import ModifyFeature from 'ol-ext/interaction/ModifyFeature'; // ol-ext UndoRedo interaction import UndoRedo from 'ol-ext/interaction/UndoRedo'; +// ol-ext SnapGuides — snaps drawing vertices to alignment guides +import SnapGuides from 'ol-ext/interaction/SnapGuides'; + // ol Select interaction (for custom multi-layer Select) import Select from 'ol/interaction/Select'; import { click as clickCondition } from 'ol/events/condition'; +// ol-ext Split interaction (for line splitting) and Toggle control +import Split from 'ol-ext/interaction/Split'; +import Toggle from 'ol-ext/control/Toggle'; +import TextButton from 'ol-ext/control/TextButton'; + +// Custom polygon split interaction +import { PolygonSplitInteraction } from '../interactions/PolygonSplitInteraction.js'; + +// Custom polygon merge interaction +import { PolygonMergeInteraction } from '../interactions/PolygonMergeInteraction.js'; + +// Custom polygon divide interaction +import { PolygonDivideInteraction } from '../interactions/PolygonDivideInteraction.js'; + +// Toast notifications +import { showToast } from '../toast.js'; + // CSS imports import 'ol/ol.css'; import 'ol-ext/dist/ol-ext.css'; @@ -123,7 +148,7 @@ export class MapView { } // Create base layers group - const baseLayers = this.createBaseLayers(options.basemap || 'osm'); + const baseLayers = this.createBaseLayers(options.basemap || 'topo'); // Markers layer this.markersLayer = new VectorLayer({ @@ -162,14 +187,58 @@ export class MapView { const layerSwitcher = new LayerSwitcher({ collapsed: true, mouseover: true, - extent: false, + extent: true, trash: false, oninfo: null, }); this.map.addControl(layerSwitcher); - // Add ScaleLine control - this.map.addControl(new ScaleLine()); + // Inject "Add Layer" button into the "External Source" group header + layerSwitcher.on('drawlist', (evt) => { + const groupTitle = (evt.layer.get('title') || '').toLowerCase(); + if (groupTitle.includes('external')) { + // Store reference to the actual External group for later use + this._externalSourceGroup = evt.layer; + const btnBar = evt.li.querySelector('.ol-layerswitcher-buttons'); + if (btnBar && !btnBar.querySelector('.ol-add-layer')) { + const addBtn = document.createElement('span'); + addBtn.className = 'ol-add-layer'; + addBtn.title = 'Add external layer'; + addBtn.textContent = '+'; + addBtn.style.cssText = ` + display:inline-flex !important;align-items:center;justify-content:center; + width:20px !important;height:20px !important;border-radius:50%; + background:#10b981 !important;color:#fff !important; + font-size:16px !important;font-weight:700; + cursor:pointer;line-height:1 !important; + margin:2px 4px 2px 2px;vertical-align:middle; + transition:background 0.2s;box-sizing:border-box; + `; + addBtn.addEventListener('mouseenter', () => { addBtn.style.background = '#059669'; }); + addBtn.addEventListener('mouseleave', () => { addBtn.style.background = '#10b981'; }); + addBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.showAddLayerDialog(); + }); + btnBar.prepend(addBtn); + } + } + }); + + // Create the add-layer dialog (hidden by default) + this._createAddLayerDialog(); + + // Create the legend panel (shows legends for visible layers that have one) + this._createLegendPanel(); + + // Add ScaleBar control + this.scaleBar = new ScaleLine({ + bar: true, + steps: 4, + text: true, + minWidth: 140, + }); + this.map.addControl(this.scaleBar); // Add GeolocationButton control const geolocationButton = new GeolocationButton({ @@ -240,6 +309,12 @@ export class MapView { // Create drawn polygon attribute popup this.createDrawnPolygonPopup(); + // Create merge identifier (UPN) chooser popup + this.createMergePopup(); + + // Create divide polygon popup (number input) + this.createDividePopup(); + // Double-click callbacks this.dblClickCallbacks = []; @@ -329,38 +404,20 @@ export class MapView { Delete: true, Info: true, Transform: true, - Split: true, - Offset: true, + Split: false, }, }); this.map.addControl(this.editBar); - // 6. Add extra buttons (Undo, Redo, Colour, Save) as a sub-bar - // inside the EditBar so they appear inline. - this._fillColor = '#f59e0b'; - const colorInput = document.createElement('input'); - colorInput.type = 'color'; - colorInput.value = this._fillColor; - colorInput.title = 'Fill colour'; - colorInput.style.cssText = 'width:28px;height:28px;border:none;padding:0;cursor:pointer;background:transparent;'; - colorInput.addEventListener('input', (e) => { - this._fillColor = e.target.value; - const hex = this._fillColor; - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - this.drawingsLayer.setStyle(new Style({ - stroke: new Stroke({ color: hex, width: 2.5 }), - fill: new Fill({ color: `rgba(${r},${g},${b},0.15)` }), - image: new Circle({ - radius: 6, - fill: new Fill({ color: hex }), - stroke: new Stroke({ color: '#fff', width: 1.5 }), - }), - })); - console.log('[MapView] Fill colour changed to', hex); - }); + // 5b. Persistent vertex overlay — when edit mode is active and the user + // selects a polygon (or line) for modification, render a small dot + // at every vertex so the user can see all editable nodes at a glance. + // ol-ext's ModifyFeature only renders the closest vertex on hover; this + // overlay complements that without subclassing the interaction. + this._setupVertexOverlay(); + // 6. Add extra buttons (Undo, Redo, Save) as a sub-bar + // inside the EditBar so they appear inline. const extraBar = new Bar({ group: true, controls: [ @@ -380,11 +437,6 @@ export class MapView { if (this._undoRedo.hasRedo()) this._undoRedo.redo(); }, }), - new Button({ - html: colorInput, - className: 'ol-colorpicker', - title: 'Fill colour', - }), new Button({ html: '', className: 'ol-save', @@ -397,6 +449,174 @@ export class MapView { }); this.editBar.addControl(extraBar); + // 6a-split. Custom Split tool with Lines / Polygons sub-categories. + // The default ol-ext Split only handles LineString. We add a parent + // Toggle with a sub-bar containing two sub-toggles: "Lines" (ol-ext + // Split) and "Polygons" (our PolygonSplitInteraction). + // No explicit sources → both interactions search ALL visible vector layers, + // so they work on drawn features, parcels, zones, and any other polygon layer. + this._lineSplitInteraction = new Split(); + this._polygonSplitInteraction = new PolygonSplitInteraction(); + this.map.addInteraction(this._lineSplitInteraction); + this.map.addInteraction(this._polygonSplitInteraction); + this._lineSplitInteraction.setActive(false); + this._polygonSplitInteraction.setActive(false); + + // When a parcel is split, the user picks which piece keeps the UPN. + this._polygonSplitInteraction.on('splitpick', (evt) => { + const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID']; + for (const feat of evt.features) { + if (feat === evt.picked) continue; + for (const field of idFields) { + if (feat.get(field) !== undefined) { + feat.set(field, ''); + } + } + } + }); + + // Polygon Divide interaction (parameter-driven equal-area division) + this._polygonDivideInteraction = new PolygonDivideInteraction(); + this.map.addInteraction(this._polygonDivideInteraction); + this._polygonDivideInteraction.setActive(false); + + const splitLineToggle = new Toggle({ + html: '', + className: 'ol-split-line', + title: 'Split Lines', + name: 'SplitLine', + interaction: this._lineSplitInteraction, + autoActivate: true, + }); + const splitPolyToggle = new Toggle({ + html: '', + className: 'ol-split-polygon', + title: 'Split Polygons', + name: 'SplitPolygon', + interaction: this._polygonSplitInteraction, + }); + const splitDivideToggle = new Toggle({ + html: '', + className: 'ol-split-divide', + title: 'Divide Polygon', + name: 'DividePolygon', + interaction: this._polygonDivideInteraction, + }); + + const splitSubBar = new Bar({ + toggleOne: true, + autoDeactivate: true, + controls: [splitLineToggle, splitPolyToggle, splitDivideToggle], + }); + + const splitParentToggle = new Toggle({ + className: 'ol-split', + title: 'Split', + name: 'Split', + bar: splitSubBar, + onToggle: (active) => { + if (!active) { + this._lineSplitInteraction.setActive(false); + this._polygonSplitInteraction.setActive(false); + this._polygonDivideInteraction.setActive(false); + } + }, + }); + this.editBar.addControl(splitParentToggle); + + // Listen for divide form request → show divide popup + this._polygonDivideInteraction.on('divideform', (evt) => { + this.showDividePopup(evt.feature, evt.source, evt.coordinate); + }); + this._polygonDivideInteraction.on('dividecancel', () => { + this.hideDividePopup(); + }); + + // When a parcel is divided, the user picks which piece keeps the UPN. + // The picked piece gets the original properties; all others get UPN cleared. + this._polygonDivideInteraction.on('dividepick', (evt) => { + const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID']; + for (const feat of evt.features) { + if (feat === evt.picked) continue; + // Clear identifier fields on the non-picked pieces + for (const field of idFields) { + if (feat.get(field) !== undefined) { + feat.set(field, ''); + } + } + } + }); + + // 6a-merge. Polygon Merge tool — select two adjacent polygons, click shared + // edges, and merge them into one. For parcels, a UPN chooser popup appears. + this._polygonMergeInteraction = new PolygonMergeInteraction(); + this.map.addInteraction(this._polygonMergeInteraction); + this._polygonMergeInteraction.setActive(false); + + const mergeToggle = new Toggle({ + html: '', + className: 'ol-merge', + title: 'Merge Polygons', + name: 'Merge', + interaction: this._polygonMergeInteraction, + }); + this.editBar.addControl(mergeToggle); + + // Listen for merged-parcel event → show UPN chooser + this._polygonMergeInteraction.on('mergedparcel', (evt) => { + this.showMergeIdentifierPopup(evt.merged, evt.propsA, evt.propsB, evt.coordinate); + }); + + // 6b. SnapGuides — shows alignment guides while drawing. + // Uses VectorImageLayer for GPU-friendly canvas rendering instead of + // re-creating individual SVG elements on every guide update. + this._snapGuidesEnabled = localStorage.getItem('snap-guides-enabled') === '1'; + this._snapGuides = new SnapGuides({ + pixelTolerance: 10, + vectorClass: VectorImageLayer, + }); + this.map.addInteraction(this._snapGuides); + + // Connect SnapGuides to whichever draw interaction becomes active. + // setDrawInteraction() only tracks one at a time, so we re-bind + // whenever a draw tool is activated. + const drawToolNames = ['DrawPoint', 'DrawLine', 'DrawPolygon', 'DrawHole', 'DrawRegular']; + for (const name of drawToolNames) { + const interaction = this.editBar.getInteraction(name); + if (interaction) { + interaction.on('change:active', () => { + if (interaction.getActive()) { + this._snapGuides.setDrawInteraction(interaction); + } + }); + } + } + + // Also connect SnapGuides to the Modify interaction for vertex editing + if (this._modifyInteraction) { + this._snapGuides.setModifyInteraction(this._modifyInteraction); + } + + // 6c. Snap-guides toggle button (magnet icon) — persisted in localStorage + const snapToggleBtn = new Button({ + html: '', + className: 'ol-snap-toggle' + (this._snapGuidesEnabled ? ' ol-active' : ''), + title: 'Toggle Snap Guides', + handleClick: () => { + this._snapGuidesEnabled = !this._snapGuidesEnabled; + localStorage.setItem('snap-guides-enabled', this._snapGuidesEnabled ? '1' : '0'); + // Update visual state + snapToggleBtn.element.classList.toggle('ol-active', this._snapGuidesEnabled); + // Activate or deactivate the interaction + if (this._snapGuides) { + this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive); + } + console.log('[MapView] Snap guides:', this._snapGuidesEnabled ? 'ON' : 'OFF'); + }, + }); + this._snapToggleBtn = snapToggleBtn; + extraBar.addControl(snapToggleBtn); + // Start hidden — use the full setEditMode(false) so the Select + // Modify interactions are deactivated (the EditBar constructor may // have re-activated them). @@ -433,7 +653,7 @@ export class MapView { this.showDrawnPolygonPopup(feature, coordinate); }); - console.log('[MapView] EditBar initialised with Drawings group, UndoRedo, colour picker'); + console.log('[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:', this._snapGuidesEnabled ? 'ON' : 'OFF', ')'); } /** @@ -495,11 +715,21 @@ export class MapView { this._modifyInteraction.setActive(this._editBarActive); } + // Toggle SnapGuides — only active when both edit mode AND the user toggle are on + if (this._snapGuides) { + this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive); + } + // Toggle TouchCursor if (this.touchCursor) { this.touchCursor.setActive(this._editBarActive); } + // Clear persistent vertex highlights when leaving edit mode + if (!this._editBarActive && this._vertexOverlaySource) { + this._vertexOverlaySource.clear(); + } + console.log('[MapView] Edit mode:', this._editBarActive ? 'ON' : 'OFF'); } @@ -511,6 +741,131 @@ export class MapView { return this._editBarActive; } + // ============================================================================ + // Persistent Vertex Highlight Overlay + // ============================================================================ + + /** + * Create a vector layer that renders a small dot at every vertex of any + * currently-selected feature (polygon, multipolygon, line, multiline). + * Only active while edit mode is on. + * + * Hooks: + * - `select` event from the Select interaction → rebuild dots for the new selection + * - `change` event on the selected feature → reposition dots when a vertex is dragged + */ + _setupVertexOverlay() { + this._vertexOverlaySource = new VectorSource(); + this._vertexOverlayLayer = new VectorLayer({ + title: '__vertex_highlight__', + source: this._vertexOverlaySource, + // Render above all other overlays but below ModifyFeature's hover indicator + zIndex: 990, + style: new Style({ + image: new Circle({ + radius: 4, + fill: new Fill({ color: 'rgba(14,165,233,0.85)' }), // brand blue + stroke: new Stroke({ color: '#fff', width: 1.2 }), + }), + }), + }); + // Hide from LayerSwitcher — purely visual, not user-toggleable + this._vertexOverlayLayer.set('displayInLayerSwitcher', false); + this.map.addLayer(this._vertexOverlayLayer); + + // Bound handler so we can attach/detach by reference + this._onSelectedFeatureGeomChange = () => this._refreshVertexOverlay(); + + // Track which feature(s) we're listening on, so we can unhook cleanly + this._vertexTrackedFeatures = new Set(); + + // When the selection changes, swap which features we listen to and rebuild dots + this._selectInteraction.on('select', () => this._refreshVertexOverlay()); + } + + /** + * Rebuild the vertex overlay from the current Select interaction's features. + * No-ops when not in edit mode. + */ + _refreshVertexOverlay() { + if (!this._vertexOverlaySource) return; + this._vertexOverlaySource.clear(); + + // Detach change listeners from previously-tracked features + if (this._vertexTrackedFeatures) { + for (const f of this._vertexTrackedFeatures) { + f.un('change', this._onSelectedFeatureGeomChange); + } + this._vertexTrackedFeatures.clear(); + } + + if (!this._editBarActive || !this._selectInteraction) return; + + const selected = this._selectInteraction.getFeatures().getArray(); + for (const feat of selected) { + const geom = feat.getGeometry(); + if (!geom) continue; + const type = geom.getType(); + if (!['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'].includes(type)) { + continue; + } + const coords = this._collectAllVertices(geom); + for (const c of coords) { + this._vertexOverlaySource.addFeature(new Feature(new Point(c))); + } + // Listen for vertex moves on this feature + feat.on('change', this._onSelectedFeatureGeomChange); + this._vertexTrackedFeatures.add(feat); + } + } + + /** + * Walk a (Multi)Polygon or (Multi)LineString geometry and return the flat + * list of vertex coordinates. Polygon rings have a duplicate closing vertex + * (last == first) which is dropped here so we don't render two dots on top + * of each other. + * + * @param {Geometry} geom + * @returns {Array>} + */ + _collectAllVertices(geom) { + const out = []; + const isCoord = (v) => Array.isArray(v) && typeof v[0] === 'number'; + + const visitRing = (ring, isPolygonRing) => { + const len = isPolygonRing && ring.length > 1 ? ring.length - 1 : ring.length; + for (let i = 0; i < len; i++) out.push(ring[i]); + }; + + const type = geom.getType(); + const coords = geom.getCoordinates(); + + switch (type) { + case 'Polygon': + // coords = [outerRing, hole1, hole2, …] + for (const ring of coords) visitRing(ring, true); + break; + case 'MultiPolygon': + // coords = [poly1, poly2, …]; each poly = [outerRing, hole1, …] + for (const poly of coords) for (const ring of poly) visitRing(ring, true); + break; + case 'LineString': + visitRing(coords, false); + break; + case 'MultiLineString': + for (const line of coords) visitRing(line, false); + break; + default: + // Fallback: deep walk to find arrays of [x, y] + const walk = (v) => { + if (isCoord(v)) out.push(v); + else if (Array.isArray(v)) for (const sub of v) walk(sub); + }; + walk(coords); + } + return out; + } + /** * Get the Drawings layer for external access. * @returns {VectorLayer} @@ -535,6 +890,16 @@ export class MapView { return this.editBar; } + /** + * Update the ScaleBar units ('metric' or 'imperial'). + * @param {'metric'|'imperial'} system + */ + setScaleBarUnits(system) { + if (this.scaleBar) { + this.scaleBar.setUnits(system === 'imperial' ? 'imperial' : 'metric'); + } + } + /** * Create the popup overlay element and add to map */ @@ -544,7 +909,8 @@ export class MapView { this.popupElement.className = 'map-popup'; this.popupElement.style.cssText = ` position: absolute; - background: white; + background: var(--card, #fff); + color: var(--card-foreground, #1e1a4b); border-radius: 8px; padding: 10px 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); @@ -689,7 +1055,8 @@ export class MapView { this.infoPopupElement.className = 'map-info-popup'; this.infoPopupElement.style.cssText = ` position: absolute; - background: white; + background: var(--card, #fff); + color: var(--card-foreground, #1e1a4b); border-radius: 10px; padding: 0; box-shadow: 0 4px 16px rgba(0,0,0,0.3); @@ -697,6 +1064,9 @@ export class MapView { font-size: 13px; min-width: 220px; max-width: 320px; + max-height: 70vh; + display: flex; + flex-direction: column; z-index: 1001; border: 1px solid var(--border, #1e1a4b1f); overflow: hidden; @@ -745,9 +1115,7 @@ export class MapView { if (geomType === 'Polygon' || geomType === 'MultiPolygon') { // Area for polygons const areaSqm = getArea(geometry, { projection: 'EPSG:3857' }); - const areaFormatted = areaSqm > 1_000_000 - ? `${(areaSqm / 1_000_000).toFixed(2)} km\u00B2 (${areaSqm.toLocaleString('en', { maximumFractionDigits: 0 })} m\u00B2)` - : `${areaSqm.toLocaleString('en', { maximumFractionDigits: 0 })} m\u00B2`; + const areaFormatted = formatAreaFull(areaSqm); rows += ` area @@ -757,9 +1125,7 @@ export class MapView { } else if (geomType === 'LineString' || geomType === 'MultiLineString') { // Length for lines const lengthM = getLength(geometry, { projection: 'EPSG:3857' }); - const lengthFormatted = lengthM >= 1000 - ? `${(lengthM / 1000).toFixed(2)} km (${lengthM.toLocaleString('en', { maximumFractionDigits: 0 })} m)` - : `${lengthM.toLocaleString('en', { maximumFractionDigits: 1 })} m`; + const lengthFormatted = formatLengthFull(lengthM); rows += ` length @@ -784,11 +1150,11 @@ export class MapView { } const html = ` -
    +
    ${this.escapeHtml(title)}
    -
    +
    ${rows}
    @@ -822,6 +1188,107 @@ export class MapView { * @param {Feature} circleFeature - The measurement circle feature (Circle geometry) * @param {Array} coordinate - Map coordinate for popup placement [x, y] */ + /** + * Collect intersection results (parcels, zones, other) into a + * structured { label, value } array for both HTML and PDF rendering. + */ + _collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer) { + const dataRows = []; + + if (parcelFeatures.length > 0) { + dataRows.push({ label: 'Parcels', value: String(parcelFeatures.length), color: '#0ea5e9' }); + } + + if (zoneFeatures.length > 0) { + const names = zoneFeatures.map(f => + f.get('colzonename') || f.get('zone_name') || f.get('name') || 'unnamed' + ); + dataRows.push({ label: 'Zones', value: String(zoneFeatures.length), color: '#7c3aed' }); + dataRows.push({ label: 'Zone Names', value: names.map(n => this.escapeHtml(n)).join(', '), color: '#7c3aed' }); + } + + for (const [title, features] of Object.entries(otherByLayer)) { + dataRows.push({ label: this.escapeHtml(title), value: `${features.length} feature(s)` }); + } + + if (dataRows.length === 0) { + dataRows.push({ label: '', value: 'No intersecting features found', empty: true }); + } + + return dataRows; + } + + /** + * Build the full popup HTML for an analysis popup (circle or area). + * + * @param {string} emoji - Header emoji + * @param {string} title - e.g. "Circle Analysis" + * @param {Array<{label:string, value:string, color?:string, empty?:boolean}>} dataRows + * @returns {string} HTML + */ + _buildAnalysisPopupHtml(emoji, title, dataRows) { + let tableRows = ''; + for (const row of dataRows) { + if (row.empty) { + tableRows += ` + + ${row.value} + `; + continue; + } + const labelColor = row.color || 'var(--muted-foreground, #7a7a7a)'; + const border = row._first ? '' : 'border-top:1px solid var(--border, #1e1a4b1f);'; + tableRows += ` + + ${row.label} + ${row.value} + `; + } + + return ` +
    + ${emoji} ${title} + +
    +
    + + ${tableRows} +
    +
    +
    + +
    `; + } + + /** + * Show the analysis popup, attach close + PDF export handlers. + */ + _showAnalysisPopup(emoji, title, dataRows, coordinate) { + this.infoPopupElement.innerHTML = this._buildAnalysisPopupHtml(emoji, title, dataRows); + this.infoPopup.setPosition(coordinate); + + this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => { + this.hideInfoPopup(); + }); + + // PDF export — dynamic import so jspdf is only loaded on demand + this.infoPopupElement.querySelector('#info-popup-export-pdf')?.addEventListener('click', () => { + // Strip HTML from values and remove the color/empty keys for the PDF + const pdfRows = dataRows + .filter(r => !r.empty) + .map(r => ({ label: r.label, value: r.value.replace(/<[^>]*>/g, '') })); + + import('../pdf-export.js').then(({ exportAnalysisPDF }) => { + exportAnalysisPDF({ title, rows: pdfRows }); + }).catch(err => { + console.error('[MapView] PDF export failed:', err); + }); + }); + } + showCircleIntersectionPopup(circleFeature, coordinate) { const circleGeom = circleFeature.getGeometry(); if (!circleGeom || typeof circleGeom.getCenter !== 'function') return; @@ -835,16 +1302,11 @@ export class MapView { // Collect intersecting features grouped by layer type const parcelFeatures = []; const zoneFeatures = []; - const otherByLayer = {}; // title → count + const otherByLayer = {}; - /** - * Check whether a feature's geometry intersects the circle polygon. - * Uses a fast extent pre-check, then a full geometry test. - */ const intersectsCircle = (feature) => { const geom = feature.getGeometry(); if (!geom) return false; - // Fast pre-check: bounding-box overlap const fExtent = geom.getExtent(); if ( fExtent[2] < circleExtent[0] || @@ -854,13 +1316,9 @@ export class MapView { ) { return false; } - // Full geometry test return circlePoly.intersectsExtent(fExtent) && this._geometriesIntersect(circlePoly, geom); }; - /** - * Walk a LayerGroup recursively and test every VectorLayer's features. - */ const scanGroup = (group, groupTitle) => { group.getLayers().forEach((layer) => { if (layer instanceof LayerGroup) { @@ -870,10 +1328,8 @@ export class MapView { const source = layer.getSource(); if (!source) return; - // Use getFeaturesInExtent for speed, then refine const candidates = source.getFeaturesInExtent(circleExtent); for (const f of candidates) { - // Skip the circle feature itself and its radius line const fType = f.get('_layerType'); if (fType === 'measure_circle' || fType === 'measure_circle_radius') continue; @@ -892,93 +1348,103 @@ export class MapView { }); }; - // Scan the overlays group scanGroup(this.overlayGroup, 'Overlays'); - // ---- Build popup HTML ---- - const radiusFormatted = radius >= 1000 - ? `${(radius / 1000).toFixed(2)} km` - : `${Math.round(radius)} m`; + // Build structured data rows + const radiusFormatted = formatLength(radius); const areaSqm = Math.PI * radius * radius; - const areaFormatted = areaSqm > 1_000_000 - ? `${(areaSqm / 1_000_000).toFixed(2)} km²` - : `${areaSqm.toLocaleString('en', { maximumFractionDigits: 0 })} m²`; + const areaFormatted = formatArea(areaSqm); - let rows = ` - - radius - ${radiusFormatted} - - - area - ${areaFormatted} - - `; + const dataRows = [ + { label: 'Radius', value: radiusFormatted, _first: true }, + { label: 'Area', value: areaFormatted }, + ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer), + ]; - // Parcels summary - if (parcelFeatures.length > 0) { - rows += ` - - parcels - ${parcelFeatures.length} - - `; - } + this._showAnalysisPopup('⭕', 'Circle Analysis', dataRows, coordinate); + } - // Zones summary - if (zoneFeatures.length > 0) { - const zoneNames = zoneFeatures.map(f => { - return f.get('colzonename') || f.get('zone_name') || f.get('name') || 'unnamed'; + /** + * Show an intersection-analysis popup for a measured area polygon. + * Same logic as showCircleIntersectionPopup but works with an + * arbitrary Polygon geometry instead of a circle. + * + * @param {Feature} polygonFeature - The measure_area feature + * @param {number[]} coordinate - Map coordinate for the popup anchor + */ + showAreaIntersectionPopup(polygonFeature, coordinate) { + const polyGeom = polygonFeature.getGeometry(); + if (!polyGeom) return; + + const polyExtent = polyGeom.getExtent(); + + // Compute area via ol/sphere for geodesic accuracy + const areaSqm = getArea(polyGeom, { projection: 'EPSG:3857' }); + const areaFormatted = formatArea(areaSqm); + + // Compute perimeter + const perimeterM = getLength(polyGeom, { projection: 'EPSG:3857' }); + const perimeterFormatted = formatLength(perimeterM); + + // Collect intersecting features grouped by layer type + const parcelFeatures = []; + const zoneFeatures = []; + const otherByLayer = {}; + + const intersectsPoly = (feature) => { + const geom = feature.getGeometry(); + if (!geom) return false; + const fExtent = geom.getExtent(); + if ( + fExtent[2] < polyExtent[0] || + fExtent[0] > polyExtent[2] || + fExtent[3] < polyExtent[1] || + fExtent[1] > polyExtent[3] + ) { + return false; + } + return polyGeom.intersectsExtent(fExtent) && this._geometriesIntersect(polyGeom, geom); + }; + + const scanGroup = (group, groupTitle) => { + group.getLayers().forEach((layer) => { + if (layer instanceof LayerGroup) { + scanGroup(layer, layer.get('title') || groupTitle); + } else if (layer instanceof VectorLayer && layer.getVisible()) { + const layerTitle = layer.get('title') || groupTitle || 'Unknown'; + const source = layer.getSource(); + if (!source) return; + + const candidates = source.getFeaturesInExtent(polyExtent); + for (const f of candidates) { + const fType = f.get('_layerType'); + if (fType === 'measure_area' || fType === 'measure_circle' || fType === 'measure_circle_radius') continue; + + if (!intersectsPoly(f)) continue; + + if (fType === 'parcel') { + parcelFeatures.push(f); + } else if (fType === 'collector_zone') { + zoneFeatures.push(f); + } else { + if (!otherByLayer[layerTitle]) otherByLayer[layerTitle] = []; + otherByLayer[layerTitle].push(f); + } + } + } }); - rows += ` - - zones - ${zoneFeatures.length} - - - zone names - ${zoneNames.map(n => this.escapeHtml(n)).join(', ')} - - `; - } + }; - // Other layers - for (const [title, features] of Object.entries(otherByLayer)) { - rows += ` - - ${this.escapeHtml(title)} - ${features.length} feature(s) - - `; - } + scanGroup(this.overlayGroup, 'Overlays'); - // Nothing found - if (parcelFeatures.length === 0 && zoneFeatures.length === 0 && Object.keys(otherByLayer).length === 0) { - rows += ` - - No intersecting features found - - `; - } + // Build structured data rows + const dataRows = [ + { label: 'Area', value: areaFormatted, _first: true }, + { label: 'Perimeter', value: perimeterFormatted }, + ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer), + ]; - const html = ` -
    - ⭕ Circle Analysis - -
    -
    - - ${rows} -
    -
    - `; - - this.infoPopupElement.innerHTML = html; - this.infoPopup.setPosition(coordinate); - - this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => { - this.hideInfoPopup(); - }); + this._showAnalysisPopup('📐', 'Area Analysis', dataRows, coordinate); } /** @@ -1040,7 +1506,8 @@ export class MapView { this.parcelEditElement.className = 'map-parcel-edit-popup'; this.parcelEditElement.style.cssText = ` position: absolute; - background: white; + background: var(--card, #fff); + color: var(--card-foreground, #1e1a4b); border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-family: var(--font-body, 'Exo', sans-serif); @@ -1097,7 +1564,7 @@ export class MapView {
    `; @@ -1179,6 +1646,264 @@ export class MapView { this._parcelEditCallbacks.push(callback); } + // ============================================================================ + // Merge Identifier (UPN) Chooser Popup + // ============================================================================ + + /** + * Create the merge identifier popup overlay. + * Shown after two parcels are merged so the user can choose which UPN to keep. + */ + createMergePopup() { + this.mergePopupElement = document.createElement('div'); + this.mergePopupElement.className = 'map-merge-popup'; + this.mergePopupElement.style.cssText = ` + position: absolute; + background: var(--card, #fff); + color: var(--card-foreground, #1e1a4b); + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 13px; + min-width: 280px; + max-width: 360px; + z-index: 1002; + border: 2px solid #10b981; + overflow: hidden; + display: flex; + flex-direction: column; + `; + + this.mergePopup = new Overlay({ + element: this.mergePopupElement, + positioning: 'bottom-center', + offset: [0, -10], + stopEvent: true, + autoPan: true, + autoPanAnimation: { duration: 250 }, + }); + + this.map.addOverlay(this.mergePopup); + } + + /** + * Show the merge identifier popup so the user can pick which parcel's + * attributes (including UPN) the merged polygon should inherit. + * + * @param {Feature} mergedFeature The newly created merged feature + * @param {Object} propsA Properties from original parcel A + * @param {Object} propsB Properties from original parcel B + * @param {Array} coordinate Map coordinate [x, y] for popup placement + */ + showMergeIdentifierPopup(mergedFeature, propsA, propsB, coordinate) { + // Extract identifiers — try common parcel ID field names + const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID']; + const getLabel = (props) => { + for (const field of idFields) { + if (props[field] !== undefined && props[field] !== null && String(props[field]).trim()) { + return { field, value: String(props[field]) }; + } + } + return { field: 'id', value: 'Unknown' }; + }; + + const labelA = getLabel(propsA); + const labelB = getLabel(propsB); + + const html = ` +
    + 🔗 Merged Parcel — Choose Identifier + +
    +
    +

    + Select which parcel's attributes the merged polygon should keep: +

    + + +
    + + +
    +
    + `; + + this.mergePopupElement.innerHTML = html; + this.mergePopup.setPosition(coordinate); + + // Close / Cancel — keep parcel A properties (the default from clone) + const close = () => { + this.mergePopup.setPosition(undefined); + }; + this.mergePopupElement.querySelector('.merge-popup-close').addEventListener('click', close); + this.mergePopupElement.querySelector('.merge-popup-cancel').addEventListener('click', close); + + // Confirm — apply chosen parcel's properties + this.mergePopupElement.querySelector('.merge-popup-confirm').addEventListener('click', () => { + const choice = this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value; + const chosenProps = choice === 'A' ? propsA : propsB; + + // Copy all properties (except geometry) onto the merged feature + const skipKeys = ['geometry']; + for (const [key, value] of Object.entries(chosenProps)) { + if (skipKeys.includes(key)) continue; + mergedFeature.set(key, value); + } + // Ensure _layerType is preserved + mergedFeature.set('_layerType', 'parcel'); + + // Notify parcel edit callbacks + for (const cb of this._parcelEditCallbacks) { + cb(mergedFeature, chosenProps); + } + + close(); + }); + + // Highlight radio labels on selection + const labels = this.mergePopupElement.querySelectorAll('label'); + const radios = this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'); + const updateHighlight = () => { + labels.forEach((lbl) => { + const radio = lbl.querySelector('input'); + lbl.style.borderColor = radio.checked ? (radio.value === 'A' ? '#0ea5e9' : '#f59e0b') : 'var(--border, #1e1a4b1f)'; + }); + }; + radios.forEach((r) => r.addEventListener('change', updateHighlight)); + updateHighlight(); + } + + // ============================================================================ + // Divide Polygon Popup (number input) + // ============================================================================ + + /** + * Create the divide polygon popup overlay. + * Shown after the user selects a polygon with the Divide tool, so they + * can enter the number of equal pieces. + */ + createDividePopup() { + this.dividePopupElement = document.createElement('div'); + this.dividePopupElement.className = 'map-divide-popup'; + this.dividePopupElement.style.cssText = ` + position: absolute; + background: var(--card, #fff); + color: var(--card-foreground, #1e1a4b); + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 13px; + min-width: 260px; + max-width: 320px; + z-index: 1002; + border: 2px solid #8b5cf6; + overflow: hidden; + display: flex; + flex-direction: column; + `; + + this.dividePopup = new Overlay({ + element: this.dividePopupElement, + positioning: 'bottom-center', + offset: [0, -10], + stopEvent: true, + autoPan: true, + autoPanAnimation: { duration: 250 }, + }); + + this.map.addOverlay(this.dividePopup); + } + + /** + * Show the divide popup so the user can enter the number of divisions. + * + * @param {Feature} feature The selected polygon feature + * @param {VectorSource} source The source containing the feature + * @param {Array} coordinate Map coordinate [x, y] for popup placement + */ + showDividePopup(feature, source, coordinate) { + const html = ` +
    + Divide Polygon + +
    +
    +

    + Enter the number of equal pieces: +

    + +
    + + +
    +
    + `; + + this.dividePopupElement.innerHTML = html; + this.dividePopup.setPosition(coordinate); + + const input = this.dividePopupElement.querySelector('.divide-input'); + input.focus(); + input.select(); + + // Close / Cancel + const cancel = () => { + this.hideDividePopup(); + this._polygonDivideInteraction.cancelDivide(); + }; + this.dividePopupElement.querySelector('.divide-popup-close').addEventListener('click', cancel); + this.dividePopupElement.querySelector('.divide-popup-cancel').addEventListener('click', cancel); + + // Confirm + this.dividePopupElement.querySelector('.divide-popup-confirm').addEventListener('click', () => { + const n = parseInt(input.value, 10); + if (!n || n < 2) { + input.style.borderColor = '#ef4444'; + return; + } + this.hideDividePopup(); + this._polygonDivideInteraction.performDivide(n); + }); + + // Allow Enter key to confirm + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.dividePopupElement.querySelector('.divide-popup-confirm').click(); + } + }); + } + + /** + * Hide the divide popup. + */ + hideDividePopup() { + this.dividePopup.setPosition(undefined); + } + // ============================================================================ // Drawn Polygon Attribute Popup // ============================================================================ @@ -1285,7 +2010,7 @@ export class MapView {
    `; @@ -1294,9 +2019,7 @@ export class MapView { // Area display const geom = feature.getGeometry(); const areaSqm = getArea(geom, { projection: 'EPSG:3857' }); - const areaFormatted = areaSqm > 1_000_000 - ? `${(areaSqm / 1_000_000).toFixed(2)} km\u00B2` - : `${areaSqm.toLocaleString('en', { maximumFractionDigits: 0 })} m\u00B2`; + const areaFormatted = formatArea(areaSqm); const html = `
    @@ -1560,6 +2283,7 @@ export class MapView { const topoLayer = new TileLayer({ title: 'Topographic', type: 'base', + zIndex: -100, visible: defaultBasemap === 'topo', source: new XYZ({ url: 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png', @@ -1568,10 +2292,12 @@ export class MapView { crossOrigin: 'anonymous', }), }); + topoLayer.set('basemapKey', 'topo'); const cartoLightLayer = new TileLayer({ title: 'Carto Light', type: 'base', + zIndex: -100, visible: defaultBasemap === 'carto-light', source: new XYZ({ url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', @@ -1580,10 +2306,12 @@ export class MapView { crossOrigin: 'anonymous', }), }); + cartoLightLayer.set('basemapKey', 'carto-light'); const cartoDarkLayer = new TileLayer({ title: 'Carto Dark', type: 'base', + zIndex: -100, visible: defaultBasemap === 'carto-dark', source: new XYZ({ url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', @@ -1592,18 +2320,24 @@ export class MapView { crossOrigin: 'anonymous', }), }); + cartoDarkLayer.set('basemapKey', 'carto-dark'); + const osmCycleLayer = new TileLayer({ title: 'OSM Cycle map', type: 'base', + zIndex: -100, visible: false, //defaultBasemap === 'osm', source: new OSM({ "url" : "https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760" }), }); + osmCycleLayer.set('basemapKey', 'cycle'); + const satelliteLayer = new TileLayer({ title: 'Satellite', type: 'base', + zIndex: -100, visible: defaultBasemap === 'satellite', source: new XYZ({ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', @@ -1612,9 +2346,11 @@ export class MapView { crossOrigin: 'anonymous', }), }); + satelliteLayer.set('basemapKey', 'satellite'); const googleLayer = new TileLayer({ title: 'Google Sat', type: 'base', + zIndex: -100, visible: defaultBasemap === 'googlesat', source: new XYZ({ // url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', @@ -1624,15 +2360,27 @@ export class MapView { crossOrigin: 'anonymous', }), }); + googleLayer.set('basemapKey', 'googlesat'); + const osmLayer = new TileLayer({ title: 'OpenStreetMap', type: 'base', + zIndex: -100, visible: defaultBasemap === 'osm', source: new OSM(), }); + osmLayer.set('basemapKey', 'osm'); + + // Remember the base-map layers so setBaseMap() can toggle visibility later + this._baseMapLayers = [ + cartoLightLayer, cartoDarkLayer, osmCycleLayer, + satelliteLayer, googleLayer, osmLayer, topoLayer, + ]; // Return LayerGroup for LayerSwitcher + // Note: ol-ext LayerSwitcher iterates layers in reverse — the LAST item + // in this array appears at the TOP of the base-map list in the UI. return new LayerGroup({ title: 'Base Maps', layers: [ @@ -1640,13 +2388,32 @@ export class MapView { cartoDarkLayer, satelliteLayer, osmCycleLayer, - topoLayer, googleLayer, osmLayer, + topoLayer, // ← displayed at the top of the base map stack ], }); } + /** + * Switch the active base map by key. + * Sets exactly one base layer visible; hides all others. + * + * @param {string} key Basemap key: 'topo' | 'osm' | 'satellite' | 'googlesat' | 'carto-light' | 'carto-dark' | 'cycle' + * @returns {boolean} true if the key matched a known base layer + */ + setBaseMap(key) { + if (!this._baseMapLayers) return false; + let matched = false; + for (const layer of this._baseMapLayers) { + const on = layer.get('basemapKey') === key; + layer.setVisible(on); + if (on) matched = true; + } + if (matched) console.log('[MapView] Base map switched to:', key); + return matched; + } + /** * Get style for a feature (handles selection state) */ @@ -2017,7 +2784,17 @@ export class MapView { const { strokeColor = '#3b82f6', strokeWidth = 2, - fillColor = 'rgba(59,130,246,0.1)', + fillColor = 'rgba(59,130,246,0.1)', + // Optional line "casing": a thicker darker stroke drawn UNDERNEATH the + // main stroke. Used for road-like layers to make light-colored lines + // visible on any base map. Set lineCasingColor to enable; the casing + // width defaults to strokeWidth + 2. + lineCasingColor = null, + lineCasingWidth = null, + pointRadius = 5, + pointFillColor = null, // defaults to strokeColor + pointStrokeColor = '#ffffff', + pointStrokeWidth = 1.5, } = styleOptions; const source = new VectorSource({ @@ -2026,13 +2803,47 @@ export class MapView { }), }); + // Build per-geometry styles. OpenLayers picks `image` for Point / + // MultiPoint, `stroke`+`fill` for Polygon / MultiPolygon, and `stroke` + // alone for LineString / MultiLineString. Putting all three on a single + // Style is enough — but a Style with only stroke+fill leaves Points + // invisible, which is what was happening on shapefile import. + const fillStyle = new Fill({ color: fillColor }); + const pointStyle = new Circle({ + radius: pointRadius, + fill: new Fill({ color: pointFillColor || strokeColor }), + stroke: new Stroke({ color: pointStrokeColor, width: pointStrokeWidth }), + }); + + // If a line casing is requested, return an array of two Styles per + // feature: the casing renders first (underneath), then the inner stroke. + // For polygons the casing also outlines them; for points the casing has + // no effect (Point geometries only render `image`). + let layerStyle; + if (lineCasingColor) { + const casingW = lineCasingWidth != null ? lineCasingWidth : strokeWidth + 2; + layerStyle = [ + new Style({ + stroke: new Stroke({ color: lineCasingColor, width: casingW }), + }), + new Style({ + stroke: new Stroke({ color: strokeColor, width: strokeWidth }), + fill: fillStyle, + image: pointStyle, + }), + ]; + } else { + layerStyle = new Style({ + stroke: new Stroke({ color: strokeColor, width: strokeWidth }), + fill: fillStyle, + image: pointStyle, + }); + } + const layer = new VectorLayer({ title: title, source: source, - style: new Style({ - stroke: new Stroke({ color: strokeColor, width: strokeWidth }), - fill: new Fill({ color: fillColor }), - }), + style: layerStyle, }); const group = targetGroup || this.overlayGroup; @@ -2068,6 +2879,540 @@ export class MapView { return group; } + /** + * Add a WMS layer to a layer group. + * + * @param {string} groupTitle Title of the target LayerGroup (e.g. 'Biophysical Environment') + * @param {string} title Display title for the layer + * @param {string} url WMS server URL + * @param {string} layers WMS LAYERS parameter + * @param {Object} [options] Extra options + * @param {string} [options.serverType='geoserver'] Server type hint ('geoserver'|'mapserver'|'qgis'|null) + * @param {string} [options.style] WMS STYLES parameter (e.g. 'colours' for DEAfrica DEM) + * @param {boolean} [options.visible=true] Initial visibility + * @param {string} [options.attributions] Attribution HTML + * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers. + * @param {number} [options.zIndex] Render z-index. Use negative values (e.g. -10) to force the + * layer behind all default-z-index layers regardless of group order. + * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible. + * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on + * while offline, explaining that the layer requires connectivity. + * @returns {TileLayer|null} The created layer, or null if group not found + */ + addWMSLayer(groupTitle, title, url, layers, options = {}) { + const group = this.getLayerGroupByTitle(groupTitle); + if (!group) { + console.warn(`[MapView] Layer group "${groupTitle}" not found — cannot add WMS layer "${title}"`); + return null; + } + + const params = { LAYERS: layers, TILED: true, WIDTH: 256, HEIGHT: 256 }; + if (options.style !== undefined) params.STYLES = options.style; + + const wmsSource = new TileWMS({ + url, + params, + serverType: options.serverType !== undefined ? options.serverType : 'geoserver', + crossOrigin: 'anonymous', + hidpi: false, + attributions: options.attributions, + }); + + const wmsLayer = new TileLayer({ + title, + visible: options.visible !== undefined ? options.visible : true, + source: wmsSource, + opacity: options.opacity !== undefined ? options.opacity : 1, + zIndex: options.zIndex, + }); + + // Show toast on tile load errors (e.g. server rejects request) + wmsSource.on('tileloaderror', () => { + showToast(`WMS layer "${title}" — tile load error. Check the URL and layer name.`, 'warning', 5000); + }); + + group.getLayers().push(wmsLayer); + + // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher + if (options.legendUrl) { + try { + this._registerLegend(wmsLayer, title, options.legendUrl); + } catch (err) { + console.warn(`[MapView] Could not register legend for "${title}":`, err); + } + } + + // Online-only warning: when the user toggles the layer on while offline, + // surface a toast explaining why nothing will render. + if (options.onlineOnly) { + this._attachOnlineOnlyHandler(wmsLayer, title); + } + + console.log(`[MapView] WMS layer added: "${title}" → group "${groupTitle}"`); + return wmsLayer; + } + + /** + * Add an XYZ tile layer to a layer group. + * + * @param {string} groupTitle Title of the target LayerGroup + * @param {string} title Display title for the layer + * @param {string} url XYZ tile URL template (with {z}/{x}/{y} placeholders) + * @param {Object} [options] Extra options + * @param {boolean} [options.visible=true] Initial visibility + * @param {string} [options.attributions] Attribution HTML + * @param {number} [options.maxZoom=19] Maximum zoom level + * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers. + * @param {number} [options.zIndex] Render z-index. Use negative values to force behind other layers. + * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible. + * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on + * while offline, explaining that the layer requires connectivity. + * @returns {TileLayer|null} The created layer, or null if group not found + */ + addXYZLayer(groupTitle, title, url, options = {}) { + const group = this.getLayerGroupByTitle(groupTitle); + if (!group) { + console.warn(`[MapView] Layer group "${groupTitle}" not found — cannot add XYZ layer "${title}"`); + return null; + } + + const xyzSource = new XYZ({ + url, + crossOrigin: 'anonymous', + maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19, + attributions: options.attributions, + }); + + const xyzLayer = new TileLayer({ + title, + visible: options.visible !== undefined ? options.visible : true, + source: xyzSource, + opacity: options.opacity !== undefined ? options.opacity : 1, + zIndex: options.zIndex, + }); + + // Show toast on tile load errors + xyzSource.on('tileloaderror', () => { + showToast(`XYZ layer "${title}" — tile load error. Check the URL.`, 'warning', 5000); + }); + + group.getLayers().push(xyzLayer); + + // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher + if (options.legendUrl) { + try { + this._registerLegend(xyzLayer, title, options.legendUrl); + } catch (err) { + console.warn(`[MapView] Could not register legend for "${title}":`, err); + } + } + + // Online-only warning: when the user toggles the layer on while offline, + // surface a toast explaining why nothing will render. + if (options.onlineOnly) { + this._attachOnlineOnlyHandler(xyzLayer, title); + } + + console.log(`[MapView] XYZ layer added: "${title}" → group "${groupTitle}"`); + return xyzLayer; + } + + // ============================================================================ + // Add External Layer Dialog + // ============================================================================ + + /** + * Create the add-layer dialog overlay (hidden by default). + * Appended to the map target element so it stays within the map viewport. + */ + _createAddLayerDialog() { + this._addLayerDialog = document.createElement('div'); + this._addLayerDialog.className = 'map-add-layer-dialog'; + this._addLayerDialog.style.cssText = ` + display:none;position:absolute;top:0;left:0;right:0;bottom:0; + z-index:1100;background:rgba(0,0,0,0.4); + align-items:center;justify-content:center; + `; + + const card = document.createElement('div'); + card.style.cssText = ` + background:var(--card, #fff);color:var(--card-foreground, #1e1a4b); + border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.35); + font-family:var(--font-body, 'Exo', sans-serif);font-size:13px; + width:340px;max-width:90vw;border:2px solid #10b981;overflow:hidden; + `; + + card.innerHTML = ` +
    + Add External Layer + +
    +
    +
    + +
    + + + +
    +
    +
    + + +
    +
    + + +
    + WMS LAYERS parameter (e.g. workspace:layer) +
    +
    +
    + + +
    +
    + + +
    +
    + `; + + this._addLayerDialog.appendChild(card); + this.map.getTargetElement().appendChild(this._addLayerDialog); + + // Type radio change — toggle layer name row visibility + const nameRow = card.querySelector('.add-layer-name-row'); + const nameHint = card.querySelector('.add-layer-name-hint'); + const urlInput = card.querySelector('.add-layer-url'); + card.querySelectorAll('input[name="add-layer-type"]').forEach((radio) => { + radio.addEventListener('change', () => { + const type = radio.value; + if (type === 'xyz') { + nameRow.style.display = 'none'; + urlInput.placeholder = 'https://example.com/tiles/{z}/{x}/{y}.png'; + } else { + nameRow.style.display = ''; + urlInput.placeholder = type === 'wms' + ? 'https://example.com/wms' + : 'https://example.com/wfs'; + nameHint.textContent = type === 'wms' + ? 'WMS LAYERS parameter (e.g. workspace:layer)' + : 'WFS typename (e.g. workspace:layer)'; + } + }); + }); + + // Close / Cancel + const close = () => this._hideAddLayerDialog(); + card.querySelector('.add-layer-close').addEventListener('click', close); + card.querySelector('.add-layer-cancel').addEventListener('click', close); + this._addLayerDialog.addEventListener('click', (e) => { + if (e.target === this._addLayerDialog) close(); + }); + + // Confirm + card.querySelector('.add-layer-confirm').addEventListener('click', () => { + const type = card.querySelector('input[name="add-layer-type"]:checked').value; + const url = card.querySelector('.add-layer-url').value.trim(); + const layerName = card.querySelector('.add-layer-name').value.trim(); + const title = card.querySelector('.add-layer-title').value.trim(); + + if (!url) { + card.querySelector('.add-layer-url').style.borderColor = '#ef4444'; + return; + } + if ((type === 'wms' || type === 'wfs') && !layerName) { + card.querySelector('.add-layer-name').style.borderColor = '#ef4444'; + return; + } + if (!title) { + card.querySelector('.add-layer-title').style.borderColor = '#ef4444'; + return; + } + + this._addExternalLayer(type, url, layerName, title); + this._hideAddLayerDialog(); + }); + + // Enter key to confirm + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + card.querySelector('.add-layer-confirm').click(); + } + if (e.key === 'Escape') { + e.preventDefault(); + close(); + } + }); + } + + /** + * Show the add-layer dialog. + */ + showAddLayerDialog() { + const dlg = this._addLayerDialog; + // Reset form + dlg.querySelector('.add-layer-url').value = ''; + dlg.querySelector('.add-layer-name').value = ''; + dlg.querySelector('.add-layer-title').value = ''; + dlg.querySelectorAll('input[name="add-layer-type"]')[0].checked = true; + dlg.querySelector('.add-layer-name-row').style.display = ''; + dlg.querySelector('.add-layer-url').placeholder = 'https://example.com/wms'; + dlg.querySelector('.add-layer-name-hint').textContent = 'WMS LAYERS parameter (e.g. workspace:layer)'; + + // Reset border colours + dlg.querySelectorAll('input[type="text"]').forEach((inp) => { + inp.style.borderColor = 'var(--border, #1e1a4b1f)'; + }); + + dlg.style.display = 'flex'; + dlg.querySelector('.add-layer-url').focus(); + } + + /** + * Hide the add-layer dialog. + */ + _hideAddLayerDialog() { + this._addLayerDialog.style.display = 'none'; + } + + /** + * Add an external layer to the "External Source" group. + * + * @param {string} type 'wms' | 'wfs' | 'xyz' + * @param {string} url Server URL + * @param {string} layerName WMS LAYERS / WFS typename (ignored for XYZ) + * @param {string} title Display title in layer switcher + */ + _addExternalLayer(type, url, layerName, title) { + const group = this._externalSourceGroup; + if (!group) { + showToast('Layer group "External Source" not found.', 'error', 4000); + return; + } + + let layer; + + switch (type) { + case 'wms': { + const wmsSrc = new TileWMS({ + url, + params: { LAYERS: layerName, TILED: true, WIDTH: 256, HEIGHT: 256 }, + serverType: 'geoserver', + crossOrigin: 'anonymous', + hidpi: false, + }); + layer = new TileLayer({ + title, + visible: true, + source: wmsSrc, + }); + wmsSrc.on('tileloaderror', () => { + showToast(`WMS "${title}" — tile load error. Check URL and layer name.`, 'warning', 5000); + }); + break; + } + + case 'wfs': { + const wfsUrl = `${url}${url.includes('?') ? '&' : '?'}` + + `service=WFS&version=1.1.0&request=GetFeature` + + `&typename=${encodeURIComponent(layerName)}` + + `&outputFormat=application/json&srsname=EPSG:3857`; + + const wfsSource = new VectorSource({ + url: wfsUrl, + format: new GeoJSON(), + }); + wfsSource.on('featuresloaderror', () => { + showToast(`WFS "${title}" — load error. Check URL and layer name.`, 'warning', 5000); + }); + + layer = new VectorLayer({ + title, + visible: true, + source: wfsSource, + style: new Style({ + stroke: new Stroke({ color: '#e11d48', width: 2 }), + fill: new Fill({ color: 'rgba(225,29,72,0.15)' }), + }), + }); + break; + } + + case 'xyz': + layer = new TileLayer({ + title, + visible: true, + source: new XYZ({ + url, + crossOrigin: 'anonymous', + }), + }); + layer.getSource().on('tileloaderror', () => { + showToast(`XYZ "${title}" — tile load error. Check the URL template.`, 'warning', 5000); + }); + break; + + default: + showToast(`Unknown layer type: ${type}`, 'error', 4000); + return; + } + + group.getLayers().push(layer); + showToast(`Layer "${title}" added to External Source.`, 'success', 3000); + console.log(`[MapView] External ${type.toUpperCase()} layer added: "${title}"`); + } + + // ============================================================================ + // Online-Only Layer Helper + // ============================================================================ + + /** + * Attach a `change:visible` listener that shows an info toast when the user + * toggles a layer ON while the device is offline. Used for layers that fetch + * tiles or features from a remote service and therefore have no useful + * cached state. + * + * The check uses navigator.onLine, which is the same signal as the rest of + * the app's online detection. + * + * @param {Layer} layer + * @param {string} title Display title used in the toast message + */ + _attachOnlineOnlyHandler(layer, title) { + layer.set('onlineOnly', true); + layer.on('change:visible', () => { + if (layer.getVisible() && !navigator.onLine) { + showToast( + `"${title}" requires an internet connection. Connect to view this layer.`, + 'info', + 5000, + ); + } + }); + } + + // ============================================================================ + // Legend Panel — shows legend images for visible layers that have one + // ============================================================================ + + /** + * Create the legend panel, positioned bottom-right inside the map target. + * Hidden when no visible layers have a registered legend. + */ + _createLegendPanel() { + this._legendPanel = document.createElement('div'); + this._legendPanel.className = 'map-legend-panel'; + this._legendPanel.style.cssText = ` + position:absolute;right:10px;bottom:40px;z-index:900; + display:none;flex-direction:column;gap:6px; + background:var(--card, #fff);color:var(--card-foreground, #1e1a4b); + border:1px solid var(--border, #1e1a4b1f);border-radius:8px; + box-shadow:0 4px 12px rgba(0,0,0,0.15); + font-family:var(--font-body, 'Exo', sans-serif);font-size:11px; + max-width:220px;max-height:60%;overflow-y:auto; + padding:8px 10px; + `; + this.map.getTargetElement().appendChild(this._legendPanel); + + // Map of layer → { wrapper, title, imgUrl } + this._legendEntries = new Map(); + } + + /** + * Register a layer's legend image and wire up visibility tracking. + * Called from addWMSLayer / addXYZLayer when a legendUrl is supplied. + * + * @param {Layer} layer The OpenLayers layer + * @param {string} title Display title for the legend header + * @param {string} legendUrl URL of the legend image + */ + _registerLegend(layer, title, legendUrl) { + if (!this._legendPanel) return; + + // Build the legend entry — a div with header + image + const wrapper = document.createElement('div'); + wrapper.className = 'map-legend-entry'; + wrapper.style.cssText = 'border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;'; + wrapper.innerHTML = ` +
    + ${this._escapeHtml(title)} +
    + ${this._escapeHtml(title)} legend + `; + + this._legendEntries.set(layer, wrapper); + + // Listen for visibility changes. Wrap in try/catch so a DOM error here + // cannot break the LayerSwitcher's click handler (which fires change:visible + // synchronously and relies on a subsequent setTimeout to update the checkbox). + const update = () => { + try { this._updateLegendPanel(); } + catch (err) { console.warn('[MapView] legend panel update failed:', err); } + }; + layer.on('change:visible', update); + + // Trigger initial state + update(); + } + + /** + * Refresh the legend panel contents: include entries for each visible + * registered layer, and show/hide the panel based on whether any are visible. + */ + _updateLegendPanel() { + if (!this._legendPanel) return; + + // Rebuild children from scratch in a stable order (Map iteration order = insertion order) + const children = []; + for (const [layer, wrapper] of this._legendEntries) { + if (layer.getVisible()) children.push(wrapper); + } + + // Remove trailing bottom-border on the last entry for a clean look + this._legendEntries.forEach((w) => { + w.style.borderBottom = '1px solid var(--border, #1e1a4b1f)'; + w.style.paddingBottom = '6px'; + }); + if (children.length > 0) { + children[children.length - 1].style.borderBottom = 'none'; + children[children.length - 1].style.paddingBottom = '0'; + } + + // Swap the DOM children + this._legendPanel.replaceChildren(...children); + this._legendPanel.style.display = children.length > 0 ? 'flex' : 'none'; + } + + /** + * Escape HTML special characters for safe text insertion. + */ + _escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + /** * Find a LayerGroup inside the overlay group by its layerId. * @@ -2114,6 +3459,49 @@ export class MapView { return this.map; } + // ============================================================================ + // Extent Helpers (used by offline-tile downloader) + // ============================================================================ + + /** + * Get the current map view's visible extent in EPSG:3857 (Web Mercator). + * @returns {Array} [minX, minY, maxX, maxY] + */ + getCurrentViewExtent() { + const view = this.map.getView(); + const size = this.map.getSize(); + if (!size) return null; + return view.calculateExtent(size); + } + + /** + * Get the bounding extent of the District Boundary layer (if present). + * Searches the overlay group for a vector layer titled "District Boundary" + * and returns the extent of its source. + * + * @returns {{ extent: Array, title: string } | null} + */ + getDistrictBoundaryExtent() { + let found = null; + const visit = (group) => { + group.getLayers().forEach((layer) => { + if (layer.getLayers) { + visit(layer); // sub-group + } else if (layer.get('title') === 'District Boundary') { + const src = layer.getSource && layer.getSource(); + if (src && typeof src.getExtent === 'function') { + const ex = src.getExtent(); + if (ex && Number.isFinite(ex[0])) { + found = { extent: ex, title: layer.get('title') }; + } + } + } + }); + }; + visit(this.overlayGroup); + return found; + } + /** * Get the marker source for advanced usage */ diff --git a/src/database.js b/src/database.js index 99501aa..402c563 100644 --- a/src/database.js +++ b/src/database.js @@ -196,6 +196,17 @@ export async function initSchema() { ) `; + // Create osm_roads table for caching the OSM road network + console.log('[Database] Creating osm_roads table...'); + await sql` + CREATE TABLE IF NOT EXISTS osm_roads ( + osm_id INTEGER PRIMARY KEY, + geometry_wkt TEXT, + properties TEXT, + fetched_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + `; + // Create indexes await sql`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`; await sql`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`; @@ -541,17 +552,20 @@ export async function getLocalCollectorZones() { export async function saveParcels(parcels) { try { await sql`DELETE FROM parcels`; + let saved = 0; for (const p of parcels) { - const props = JSON.stringify(p); - // API field names may vary — try common WKT field names - const wkt = p.polygon || p.boundary || p.geom || p.wkt || ''; const id = p.id || p.parcelid || p.parcel_id || null; + if (id == null) continue; // skip rows without a usable ID + const props = JSON.stringify(p); + // API field names: 'boundary' (WKT), 'polygon', 'geom', 'wkt' + const wkt = p.boundary || p.polygon || p.geom || p.wkt || ''; await sql` - INSERT INTO parcels (id, geometry_wkt, properties, fetched_at) + INSERT OR REPLACE INTO parcels (id, geometry_wkt, properties, fetched_at) VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP) `; + saved++; } - console.log('[Database] ✓ Saved', parcels.length, 'parcels'); + console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'duplicates replaced)'); } catch (error) { console.error('[Database] ✗ Failed to save parcels:', error); throw error; @@ -680,6 +694,62 @@ export async function getLocalBuildingFootprints() { } } +/** + * Save OSM roads to the local SQLite table. + * Replaces all existing rows. + * + * @param {Array} roads - Array of road objects from the API + */ +export async function saveOSMRoads(roads) { + try { + if (roads.length > 0) { + const first = roads[0]; + const types = {}; + for (const [k, v] of Object.entries(first)) { + types[k] = v === null ? 'null' : typeof v; + } + console.log('[Database] First road field types:', types); + } + + await sql`DELETE FROM osm_roads`; + for (const r of roads) { + const props = JSON.stringify(r); + + // Geometry — may arrive as WKT string or GeoJSON object + let rawWkt = r.geom || r.geometry || r.wkt || r.road || r.line || ''; + const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt); + + // osm_id must be a primitive — fall back to null if missing or malformed + let rawId = r.osm_id ?? r.osmid ?? r.id ?? null; + const osmId = (rawId !== null && typeof rawId === 'object') ? null : rawId; + + await sql` + INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at) + VALUES (${osmId}, ${wkt}, ${props}, CURRENT_TIMESTAMP) + `; + } + console.log('[Database] ✓ Saved', roads.length, 'OSM roads'); + } catch (error) { + console.error('[Database] ✗ Failed to save OSM roads:', error); + throw error; + } +} + +/** + * Load all cached OSM roads from the local table. + * @returns {Promise} Array of road objects, or null if empty + */ +export async function getLocalOSMRoads() { + try { + const rows = await sql`SELECT properties FROM osm_roads ORDER BY osm_id`; + if (rows.length === 0) return null; + return rows.map(r => JSON.parse(r.properties)); + } catch (error) { + console.error('[Database] ✗ Failed to read local OSM roads:', error); + return null; + } +} + // ============================================================================ // Export / Import // ============================================================================ @@ -769,6 +839,90 @@ export async function getDatabaseStatus() { } } +// ============================================================================ +// Cached-Layer Management +// ============================================================================ + +/** + * Tables that hold data fetched from the server. + * + * Clearing these is safe — the corresponding loaders (loadParcels, + * loadBuildingFootprints, loadOSMRoads, loadCollectorZones, …) will re-fetch + * the data from the API on the next app start. + * + * NOT included: user-created tables (`locations`, `pending_changes`) — those + * hold local work that must not be auto-deleted. + */ +export const CACHED_LAYER_TABLES = Object.freeze([ + 'parcels', + 'building_footprints', + 'osm_roads', + 'collector_zones', + 'remote_data', +]); + +/** + * Check whether a table name is in the cleared-layer allow-list. + * @param {string} tableName + * @returns {boolean} + */ +export function isCachedLayerTable(tableName) { + return CACHED_LAYER_TABLES.includes(tableName); +} + +/** + * Delete all rows from a single cached-layer table. + * Rejects unknown table names so this can't be abused to drop user data. + * + * @param {string} tableName - One of CACHED_LAYER_TABLES + * @returns {Promise} Number of rows that were in the table before deletion + */ +export async function clearTable(tableName) { + if (!isCachedLayerTable(tableName)) { + throw new Error(`Refusing to clear "${tableName}" — not a known cached-layer table`); + } + + const before = await sql(`SELECT COUNT(*) AS n FROM "${tableName}"`); + const count = before[0]?.n ?? 0; + + await sql(`DELETE FROM "${tableName}"`); + console.log(`[Database] ✓ Cleared "${tableName}" (${count} rows)`); + broadcastChange(tableName, 'CLEAR', null); + return count; +} + +/** + * Clear every cached-layer table (whatever exists in this database). + * Tables that don't exist yet are skipped silently. + * + * @returns {Promise<{ table: string, count: number }[]>} per-table report + */ +export async function clearAllCachedLayers() { + const existing = await sql` + SELECT name FROM sqlite_master + WHERE type='table' AND name IN ( + 'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'remote_data' + ) + `; + const existingNames = new Set(existing.map((r) => r.name)); + + const results = []; + for (const tableName of CACHED_LAYER_TABLES) { + if (!existingNames.has(tableName)) continue; + try { + const count = await clearTable(tableName); + results.push({ table: tableName, count }); + } catch (err) { + console.error(`[Database] Failed to clear ${tableName}:`, err); + results.push({ table: tableName, count: 0, error: err.message }); + } + } + + const total = results.reduce((s, r) => s + r.count, 0); + console.log(`[Database] ✓ Cleared all cached layers: ${total} rows across ${results.length} tables`); + return results; +} + /** * Get a list of all tables with their row counts. * @returns {Promise>} @@ -890,6 +1044,12 @@ export default { insertNewParcel, saveBuildingFootprints, getLocalBuildingFootprints, + saveOSMRoads, + getLocalOSMRoads, + CACHED_LAYER_TABLES, + isCachedLayerTable, + clearTable, + clearAllCachedLayers, exportDatabase, exportToGeoJSON, importDatabase, diff --git a/src/geom/polygonDivide.js b/src/geom/polygonDivide.js new file mode 100644 index 0000000..b0fa267 --- /dev/null +++ b/src/geom/polygonDivide.js @@ -0,0 +1,295 @@ +/** + * Pure geometry functions for dividing a polygon into N equal-area pieces. + * + * No OpenLayers dependency — operates on raw coordinate arrays. + * + * The algorithm finds the polygon's longest edge, then places N-1 cutting + * lines perpendicular to that edge. Each cutting-line position is found + * via binary search so that the piece it cuts off has exactly 1/N of the + * remaining area. The actual cut is delegated to `splitPolygonByLine()`. + */ + +import { splitPolygonByLine } from './polygonSplit.js'; + +// ── Utility helpers (self-contained) ───────────────────────────────────────── + +function dist2(a, b) { + return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2; +} + +/** + * Signed area of a ring (shoelace formula). + */ +function signedArea(ring) { + let area = 0; + for (let i = 0, n = ring.length; i < n - 1; i++) { + area += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1]; + } + return area / 2; +} + +/** + * Absolute polygon area, accounting for holes. + */ +function polygonArea(coords) { + let area = Math.abs(signedArea(coords[0])); + for (let i = 1; i < coords.length; i++) { + area -= Math.abs(signedArea(coords[i])); + } + return area; +} + +/** + * Find the longest edge of a ring and return direction vectors. + * + * @param {number[][]} ring Closed ring + * @returns {{ p0: number[], p1: number[], along: number[], perp: number[] }} + * `along` = unit vector along the longest edge, + * `perp` = unit vector perpendicular to `along` (rotated 90° CCW) + */ +function longestEdge(ring) { + const n = ring.length - 1; // unique vertices + let bestLen = -1; + let bestI = 0; + + for (let i = 0; i < n; i++) { + const d = dist2(ring[i], ring[i + 1]); + if (d > bestLen) { + bestLen = d; + bestI = i; + } + } + + const p0 = ring[bestI]; + const p1 = ring[bestI + 1]; + const len = Math.sqrt(bestLen); + const along = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len]; + // Perpendicular: rotate 90° CCW + const perp = [-along[1], along[0]]; + + return { p0, p1, along, perp }; +} + +/** + * Build a cutting line perpendicular to `along` at parameter `t`. + * + * The line passes through `origin + t * along` and extends `extent` units + * in both `perp` directions — long enough to fully cross the polygon. + */ +function makeCuttingLine(origin, along, perp, t, extent) { + const cx = origin[0] + t * along[0]; + const cy = origin[1] + t * along[1]; + return [ + [cx - extent * perp[0], cy - extent * perp[1]], + [cx + extent * perp[0], cy + extent * perp[1]], + ]; +} + +/** + * Project the centroid of a polygon's exterior ring onto the `along` axis. + * Returns the scalar parameter `t` relative to `origin`. + */ +function centroidT(coords, origin, along) { + const ring = coords[0]; + const n = ring.length - 1; + let sx = 0, sy = 0; + for (let i = 0; i < n; i++) { + sx += ring[i][0]; + sy += ring[i][1]; + } + const cx = sx / n - origin[0]; + const cy = sy / n - origin[1]; + return cx * along[0] + cy * along[1]; +} + +// ── Main export ────────────────────────────────────────────────────────────── + +/** + * Divide a polygon into N equal-area pieces by parallel cuts perpendicular + * to a user-selected edge. + * + * @param {number[][][]} polygonCoords Polygon coordinates [ring, ...holes] + * @param {number} n Number of pieces (must be >= 1) + * @param {number[][]} edgeCoords The selected edge `[p0, p1]` — cuts will be + * perpendicular to this edge direction. + * @returns {{ pieces: number[][][][], error?: undefined } | { pieces: null, error: string }} + */ +export function dividePolygon(polygonCoords, n, edgeCoords) { + if (!Number.isInteger(n) || n < 1) { + return { pieces: null, error: 'Number of divisions must be a positive integer.' }; + } + if (n === 1) { + return { pieces: [polygonCoords] }; + } + + const ring = polygonCoords[0]; + const totalArea = polygonArea(polygonCoords); + + if (totalArea < 1e-6) { + return { pieces: null, error: 'Polygon has no measurable area.' }; + } + + // 1. Determine cutting direction from the selected edge + let p0, along, perp; + if (edgeCoords && edgeCoords.length === 2) { + p0 = edgeCoords[0]; + const dx = edgeCoords[1][0] - edgeCoords[0][0]; + const dy = edgeCoords[1][1] - edgeCoords[0][1]; + const len = Math.sqrt(dx * dx + dy * dy); + if (len < 1e-10) { + return { pieces: null, error: 'Selected edge has zero length.' }; + } + along = [dx / len, dy / len]; + perp = [-along[1], along[0]]; + } else { + // Fallback: use longest edge + const edge = longestEdge(ring); + p0 = edge.p0; + along = edge.along; + perp = edge.perp; + } + const origin = p0; + + // 2. Project all vertices onto the `along` axis to find extent + const nVerts = ring.length - 1; + let tMin = Infinity, tMax = -Infinity; + for (let i = 0; i < nVerts; i++) { + const dx = ring[i][0] - origin[0]; + const dy = ring[i][1] - origin[1]; + const t = dx * along[0] + dy * along[1]; + if (t < tMin) tMin = t; + if (t > tMax) tMax = t; + } + + // Cutting line extent: enough to cross the polygon in the `perp` direction + let perpMin = Infinity, perpMax = -Infinity; + for (let i = 0; i < nVerts; i++) { + const dx = ring[i][0] - origin[0]; + const dy = ring[i][1] - origin[1]; + const p = dx * perp[0] + dy * perp[1]; + if (p < perpMin) perpMin = p; + if (p > perpMax) perpMax = p; + } + const extent = (perpMax - perpMin) * 1.5; // generous overshoot + + // 3. Iteratively cut pieces + const pieces = []; + let remaining = polygonCoords; + let remainingCount = n; + + for (let i = 0; i < n - 1; i++) { + const remainingArea = polygonArea(remaining); + const targetArea = remainingArea / remainingCount; + + // Re-project the remaining polygon to get its current t-range + const remRing = remaining[0]; + const remN = remRing.length - 1; + let rMin = Infinity, rMax = -Infinity; + for (let j = 0; j < remN; j++) { + const dx = remRing[j][0] - origin[0]; + const dy = remRing[j][1] - origin[1]; + const t = dx * along[0] + dy * along[1]; + if (t < rMin) rMin = t; + if (t > rMax) rMax = t; + } + + // Binary search for the cutting position + let lo = rMin; + let hi = rMax; + let bestT = (lo + hi) / 2; + let bestPiece = null; + let bestRemaining = null; + let bestError = Infinity; + + for (let iter = 0; iter < 40; iter++) { + const mid = (lo + hi) / 2; + const line = makeCuttingLine(origin, along, perp, mid, extent); + const result = splitPolygonByLine(remaining, line); + + if (!result) { + // Cutting line didn't produce a valid split — nudge and retry + // Try slightly shifted positions + const nudge = (hi - lo) * 0.01; + const lineA = makeCuttingLine(origin, along, perp, mid + nudge, extent); + const resultA = splitPolygonByLine(remaining, lineA); + if (resultA) { + const [halfA, halfB] = resultA; + const tA = centroidT(halfA, origin, along); + const tB = centroidT(halfB, origin, along); + const nearPiece = tA < tB ? halfA : halfB; + const farPiece = tA < tB ? halfB : halfA; + const nearArea = polygonArea(nearPiece); + const err = Math.abs(nearArea - targetArea); + if (err < bestError) { + bestError = err; + bestT = mid + nudge; + bestPiece = nearPiece; + bestRemaining = farPiece; + } + } + // Try the other direction + const lineB = makeCuttingLine(origin, along, perp, mid - nudge, extent); + const resultB = splitPolygonByLine(remaining, lineB); + if (resultB) { + const [halfA, halfB] = resultB; + const tA = centroidT(halfA, origin, along); + const tB = centroidT(halfB, origin, along); + const nearPiece = tA < tB ? halfA : halfB; + const farPiece = tA < tB ? halfB : halfA; + const nearArea = polygonArea(nearPiece); + const err = Math.abs(nearArea - targetArea); + if (err < bestError) { + bestError = err; + bestT = mid - nudge; + bestPiece = nearPiece; + bestRemaining = farPiece; + } + } + // Bisect anyway to keep converging + lo = mid; + continue; + } + + const [halfA, halfB] = result; + const tA = centroidT(halfA, origin, along); + const tB = centroidT(halfB, origin, along); + const nearPiece = tA < tB ? halfA : halfB; + const farPiece = tA < tB ? halfB : halfA; + const nearArea = polygonArea(nearPiece); + + const err = Math.abs(nearArea - targetArea); + if (err < bestError) { + bestError = err; + bestT = mid; + bestPiece = nearPiece; + bestRemaining = farPiece; + } + + // Converged? + if (err / remainingArea < 0.001) break; + + // Adjust search range + if (nearArea < targetArea) { + lo = mid; // need to cut farther out + } else { + hi = mid; // need to cut closer + } + } + + if (!bestPiece || !bestRemaining) { + return { + pieces: null, + error: `Could not find a valid cut for piece ${i + 1} of ${n}. The polygon shape may be too irregular for equal division.`, + }; + } + + pieces.push(bestPiece); + remaining = bestRemaining; + remainingCount--; + } + + // The last remaining piece is the Nth piece + pieces.push(remaining); + + return { pieces }; +} diff --git a/src/geom/polygonMerge.js b/src/geom/polygonMerge.js new file mode 100644 index 0000000..955b2d0 --- /dev/null +++ b/src/geom/polygonMerge.js @@ -0,0 +1,407 @@ +/** + * Pure geometry functions for merging two adjacent polygons. + * + * No OpenLayers dependency — operates on raw coordinate arrays. + */ + +/** + * Squared distance between two points. + */ +function dist2(a, b) { + return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2; +} + +/** + * Signed area of a ring (shoelace formula). + * Positive = counter-clockwise, negative = clockwise. + */ +function signedArea(ring) { + let area = 0; + for (let i = 0, n = ring.length; i < n - 1; i++) { + area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]); + } + return area / 2; +} + +/** + * Test whether a point is inside a ring (ray-casting algorithm). + */ +function pointInRing(pt, ring) { + let inside = false; + for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) { + const xi = ring[i][0], yi = ring[i][1]; + const xj = ring[j][0], yj = ring[j][1]; + if (((yi > pt[1]) !== (yj > pt[1])) && + (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) { + inside = !inside; + } + } + return inside; +} + +/** + * Ensure a ring has the desired winding order. + */ +function ensureWinding(ring, ccw) { + const area = signedArea(ring); + if ((ccw && area < 0) || (!ccw && area > 0)) { + return ring.slice().reverse(); + } + return ring; +} + +/** + * Close a ring (ensure first === last). + */ +function closeRing(coords) { + if (coords.length < 2) return coords; + if (dist2(coords[0], coords[coords.length - 1]) > 1e-10) { + return [...coords, coords[0].slice()]; + } + return coords; +} + +/** + * Perpendicular distance from a point to a segment. + * + * @param {number[]} pt + * @param {number[]} segA Segment start + * @param {number[]} segB Segment end + * @returns {number} Squared distance + */ +function distToSegmentSq(pt, segA, segB) { + const dx = segB[0] - segA[0]; + const dy = segB[1] - segA[1]; + const lenSq = dx * dx + dy * dy; + + if (lenSq < 1e-20) return dist2(pt, segA); // degenerate segment + + // Parametric position of the projection + let t = ((pt[0] - segA[0]) * dx + (pt[1] - segA[1]) * dy) / lenSq; + t = Math.max(0, Math.min(1, t)); + + const projX = segA[0] + t * dx; + const projY = segA[1] + t * dy; + return (pt[0] - projX) ** 2 + (pt[1] - projY) ** 2; +} + +/** + * Find the ring edge closest to a click coordinate. + * + * @param {number[][]} ring Closed ring + * @param {number[]} clickCoord [x, y] + * @returns {{ segIdx: number, distSq: number }} + */ +function findClosestEdge(ring, clickCoord) { + let bestIdx = 0; + let bestDist = Infinity; + const n = ring.length - 1; // unique vertices + + for (let i = 0; i < n; i++) { + const d = distToSegmentSq(clickCoord, ring[i], ring[(i + 1) % n === 0 ? n : i + 1]); + if (d < bestDist) { + bestDist = d; + bestIdx = i; + } + } + return { segIdx: bestIdx, distSq: bestDist }; +} + +/** + * Check if two coordinates are equal within tolerance. + */ +function coordsEqual(a, b, tolSq) { + return dist2(a, b) < tolSq; +} + +/** + * Test whether a point lies within tolerance of any edge of a ring. + * + * Unlike coordsEqual (vertex-to-vertex), this checks whether the point is + * close to the ring's *boundary* — it projects onto segments, so it works + * even when the two polygons have different vertex density along the shared + * edge or when vertices are slightly offset from separate digitisation. + * + * @param {number[]} pt Point to test + * @param {number[][]} ring Closed ring + * @param {number} tolSq Squared distance tolerance + * @returns {boolean} + */ +function isVertexNearRing(pt, ring, tolSq) { + const n = ring.length - 1; + for (let i = 0; i < n; i++) { + if (distToSegmentSq(pt, ring[i], ring[i + 1]) < tolSq) { + return true; + } + } + return false; +} + +/** + * Find the shared boundary between two polygon rings. + * + * Adjacent polygons share edges in reverse winding direction: + * if A walks P→Q along the shared boundary, B walks Q→P. + * + * The algorithm has two stages: + * + * 1. **Seed validation** — uses `isVertexNearRing` (vertex-to-edge proximity) + * to confirm the user-clicked edges actually lie on a common boundary. + * This is the forgiving check that handles offset vertices from separate + * digitisation. + * + * 2. **Lockstep extension** — walks both rings together (A forward, B in + * the opposite direction) and extends the shared boundary one vertex at + * a time. Three cases are tried at each step: + * a) Both rings advance: vertex-to-vertex match (classic case). + * b) Only A advances: A has an extra vertex that projects onto B's + * frontier edge (different vertex density). + * c) Only B advances: vice-versa. + * Because extension is coupled to the *frontier edge* of the other ring + * (not the entire ring), it cannot overshoot into non-shared territory, + * even for small or closely-spaced polygons. + * + * @param {number[][]} ringA Closed ring + * @param {number[][]} ringB Closed ring + * @param {number} seedIdxA Seed edge index on ring A + * @param {number} seedIdxB Seed edge index on ring B + * @param {number} tolerance Distance tolerance (in map units) + * @returns {{ startA: number, endA: number, startB: number, endB: number, reversed: boolean } | null} + */ +function findSharedBoundary(ringA, ringB, seedIdxA, seedIdxB, tolerance) { + const nA = ringA.length - 1; // unique vertices + const nB = ringB.length - 1; + const tolSq = tolerance * tolerance; + + // ── Validate seed edges ───────────────────────────────────────────── + // Both vertices of the seed edge on A must be near ring B's boundary, + // or both vertices of the seed edge on B must be near ring A's boundary. + // This uses vertex-to-edge proximity so it handles offset digitisation. + const a0 = ringA[seedIdxA]; + const a1 = ringA[(seedIdxA + 1) % nA]; + const b0 = ringB[seedIdxB]; + const b1 = ringB[(seedIdxB + 1) % nB]; + + const a0NearB = isVertexNearRing(a0, ringB, tolSq); + const a1NearB = isVertexNearRing(a1, ringB, tolSq); + const b0NearA = isVertexNearRing(b0, ringA, tolSq); + const b1NearA = isVertexNearRing(b1, ringA, tolSq); + + if (!(a0NearB && a1NearB) && !(b0NearA && b1NearA)) { + console.warn('[polygonMerge] Seed edges are not on the shared boundary'); + return null; + } + + // ── Determine winding direction ───────────────────────────────────── + // Reversed (the normal case): A's a0 ≈ B's b1 and A's a1 ≈ B's b0. + let reversed; + if (coordsEqual(a0, b1, tolSq) && coordsEqual(a1, b0, tolSq)) { + reversed = true; + } else if (coordsEqual(a0, b0, tolSq) && coordsEqual(a1, b1, tolSq)) { + reversed = false; + } else { + // Vertices don't match exactly — use proximity to decide direction + reversed = dist2(a0, b1) < dist2(a0, b0); + } + + // ── Initialise shared boundary ────────────────────────────────────── + let startA = seedIdxA; + let endA = (seedIdxA + 1) % nA; + let startB, endB; + + if (reversed) { + // A walks startA → endA, B walks startB ← endB (reversed ring order) + startB = (seedIdxB + 1) % nB; + endB = seedIdxB; + } else { + startB = seedIdxB; + endB = (seedIdxB + 1) % nB; + } + + // ── Extend forward (endA++, endB-- if reversed) ───────────────────── + // Walk both rings in lockstep. At each step try three strategies: + // 1. Both advance — vertices match (vertex-to-vertex). + // 2. Only A advances — A's next vertex projects onto B's frontier edge. + // 3. Only B advances — B's next vertex projects onto A's frontier edge. + let safety = nA + nB; + while (safety-- > 0) { + const nextA = (endA + 1) % nA; + const nextB = reversed ? (endB - 1 + nB) % nB : (endB + 1) % nB; + if (nextA === startA || nextB === startB) break; // wrapped around + + // Case 1: vertex-to-vertex match + if (coordsEqual(ringA[nextA], ringB[nextB], tolSq)) { + endA = nextA; + endB = nextB; + continue; + } + + // Case 2: A has extra vertex — project onto B's frontier edge + if (distToSegmentSq(ringA[nextA], ringB[endB], ringB[nextB]) < tolSq) { + endA = nextA; + continue; + } + + // Case 3: B has extra vertex — project onto A's frontier edge + if (distToSegmentSq(ringB[nextB], ringA[endA], ringA[nextA]) < tolSq) { + endB = nextB; + continue; + } + + break; // no match — end of shared boundary + } + + // ── Extend backward (startA--, startB++ if reversed) ──────────────── + safety = nA + nB; + while (safety-- > 0) { + const prevA = (startA - 1 + nA) % nA; + const prevB = reversed ? (startB + 1) % nB : (startB - 1 + nB) % nB; + if (prevA === endA || prevB === endB) break; + + // Case 1: vertex-to-vertex match + if (coordsEqual(ringA[prevA], ringB[prevB], tolSq)) { + startA = prevA; + startB = prevB; + continue; + } + + // Case 2: A has extra vertex — project onto B's frontier edge + if (distToSegmentSq(ringA[prevA], ringB[startB], ringB[prevB]) < tolSq) { + startA = prevA; + continue; + } + + // Case 3: B has extra vertex — project onto A's frontier edge + if (distToSegmentSq(ringB[prevB], ringA[startA], ringA[prevA]) < tolSq) { + startB = prevB; + continue; + } + + break; + } + + return { startA, endA, startB, endB, reversed }; +} + +/** + * Walk a ring from startIdx to endIdx (exclusive), going forward and wrapping. + * Skips startIdx and stops before reaching endIdx. + * Returns the vertices of the non-shared portion. + * + * @param {number[][]} ring Closed ring + * @param {number} fromIdx Start walking from this index (inclusive) + * @param {number} toIdx Stop at this index (inclusive) + * @returns {number[][]} + */ +function walkRing(ring, fromIdx, toIdx) { + const n = ring.length - 1; + const result = []; + let idx = fromIdx; + while (true) { + result.push(ring[idx]); + if (idx === toIdx) break; + idx = (idx + 1) % n; + // Safety: prevent infinite loops + if (result.length > n + 1) break; + } + return result; +} + +/** + * Merge two adjacent polygons along their shared boundary. + * + * @param {number[][][]} polygonCoordsA Polygon A coordinates [exteriorRing, ...holes] + * @param {number[][][]} polygonCoordsB Polygon B coordinates [exteriorRing, ...holes] + * @param {number[]} clickCoordA Click coordinate on the shared edge of polygon A + * @param {number[]} clickCoordB Click coordinate on the shared edge of polygon B + * @param {number} [tolerance=5] Distance tolerance in map units (default 5 metres in EPSG:3857). + * A larger tolerance handles polygons that were digitised separately and + * whose shared vertices don't coincide exactly. + * @returns {{ coords: number[][][], error?: undefined } | { coords: null, error: string }} + * On success: `{ coords: [...] }`. On failure: `{ coords: null, error: 'reason' }`. + */ +export function mergePolygons(polygonCoordsA, polygonCoordsB, clickCoordA, clickCoordB, tolerance = 5) { + const ringA = polygonCoordsA[0]; + const ringB = polygonCoordsB[0]; + const holesA = polygonCoordsA.slice(1); + const holesB = polygonCoordsB.slice(1); + + // 1. Find seed edges (closest to user clicks) + const seedA = findClosestEdge(ringA, clickCoordA); + const seedB = findClosestEdge(ringB, clickCoordB); + + // 2. Find shared boundary + const shared = findSharedBoundary(ringA, ringB, seedA.segIdx, seedB.segIdx, tolerance); + if (!shared) { + console.warn('[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring'); + return { coords: null, error: 'The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons.' }; + } + + const { startA, endA, startB, endB, reversed } = shared; + const nA = ringA.length - 1; + const nB = ringB.length - 1; + + // 3. Stitch the non-shared portions together + // A's shared goes from startA → endA. Non-shared: endA → startA (forward, wrapping). + // B's shared (reversed case) goes from startB backward to endB. + // Non-shared: startB → endB (forward). + // B's shared (same-dir case) goes from startB → endB. + // Non-shared: endB → startB (forward, wrapping). + // + // The last vertex of partA (startA) must coincide with the first vertex + // of partB for a clean join. + + const partA = walkRing(ringA, endA, startA); + + let partB; + if (reversed) { + // B's non-shared goes from startB forward to endB. + // startB vertex ≈ startA vertex (they meet at one end of the shared boundary). + partB = walkRing(ringB, startB, endB); + } else { + // B's non-shared goes from endB forward to startB. + partB = walkRing(ringB, endB, startB); + } + + // partA ends at startA, partB starts at a vertex that should coincide. + // Skip the first vertex of partB to avoid the duplicate junction point. + const merged = [...partA, ...partB.slice(1)]; + + // Snap the closing junction. With non-coincident vertices (separate + // digitisation) the last vertex of partB may be a few metres from the + // first vertex of partA (ringA[endA]). Replace it to avoid a tiny + // sliver edge that closeRing would otherwise create. + const tolSq = tolerance * tolerance; + if (merged.length > 2 && dist2(merged[merged.length - 1], merged[0]) < tolSq) { + merged[merged.length - 1] = merged[0].slice(); + } + + const mergedRing = closeRing(merged); + + // 4. Validate: the merged ring should have a reasonable area + const areaA = Math.abs(signedArea(ringA)); + const areaB = Math.abs(signedArea(ringB)); + const areaMerged = Math.abs(signedArea(mergedRing)); + const expectedArea = areaA + areaB; + + // Allow 10% tolerance for area mismatch (shared edges can cause slight differences) + if (areaMerged < expectedArea * 0.5 || areaMerged > expectedArea * 1.5) { + console.warn(`[polygonMerge] Area mismatch: A=${areaA.toFixed(1)}, B=${areaB.toFixed(1)}, merged=${areaMerged.toFixed(1)}, expected≈${expectedArea.toFixed(1)}`); + return { coords: null, error: 'Merge produced an invalid polygon (area mismatch). The polygons may not be truly adjacent — try clicking closer to the shared boundary.' }; + } + + // 5. Match winding order to original + const originalCCW = signedArea(ringA) > 0; + const finalRing = ensureWinding(mergedRing, originalCCW); + + // 6. Collect holes from both polygons + const allHoles = [...holesA, ...holesB]; + // Filter: only include holes that actually fall inside the merged ring + const validHoles = allHoles.filter(hole => { + const cx = hole.reduce((s, p) => s + p[0], 0) / (hole.length - 1); + const cy = hole.reduce((s, p) => s + p[1], 0) / (hole.length - 1); + return pointInRing([cx, cy], finalRing); + }); + + return { coords: [finalRing, ...validHoles] }; +} diff --git a/src/geom/polygonSplit.js b/src/geom/polygonSplit.js new file mode 100644 index 0000000..b5444b9 --- /dev/null +++ b/src/geom/polygonSplit.js @@ -0,0 +1,395 @@ +/** + * Pure geometry functions for splitting a polygon by a line. + * + * No OpenLayers dependency — operates on raw coordinate arrays. + */ + +/** + * Compute the intersection point of two 2D line segments. + * Segment A: p1→p2, Segment B: p3→p4. + * + * @param {number[]} p1 + * @param {number[]} p2 + * @param {number[]} p3 + * @param {number[]} p4 + * @param {number} [eps=1e-10] tolerance for parallel check + * @returns {{ point: number[], t: number, u: number } | null} + * t = parametric position on segment A (0–1), + * u = parametric position on segment B (0–1) + */ +function segmentIntersection(p1, p2, p3, p4, eps = 1e-10) { + const dx1 = p2[0] - p1[0]; + const dy1 = p2[1] - p1[1]; + const dx2 = p4[0] - p3[0]; + const dy2 = p4[1] - p3[1]; + + const denom = dx1 * dy2 - dy1 * dx2; + if (Math.abs(denom) < eps) return null; // parallel / collinear + + const dx3 = p3[0] - p1[0]; + const dy3 = p3[1] - p1[1]; + + const t = (dx3 * dy2 - dy3 * dx2) / denom; + const u = (dx3 * dy1 - dy3 * dx1) / denom; + + if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null; + + return { + point: [p1[0] + t * dx1, p1[1] + t * dy1], + t: Math.max(0, Math.min(1, t)), + u: Math.max(0, Math.min(1, u)), + }; +} + +/** + * Signed area of a ring (shoelace formula). + * Positive = counter-clockwise, negative = clockwise. + */ +function signedArea(ring) { + let area = 0; + for (let i = 0, n = ring.length; i < n - 1; i++) { + area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]); + } + return area / 2; +} + +/** + * Test whether a point is inside a ring (ray-casting algorithm). + */ +function pointInRing(pt, ring) { + let inside = false; + for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) { + const xi = ring[i][0], yi = ring[i][1]; + const xj = ring[j][0], yj = ring[j][1]; + if (((yi > pt[1]) !== (yj > pt[1])) && + (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) { + inside = !inside; + } + } + return inside; +} + +/** + * Squared distance between two points. + */ +function dist2(a, b) { + return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2; +} + +/** + * Find all intersection points between a cutting line and a polygon ring. + * + * @param {number[][]} ring Closed ring coordinates (first === last) + * @param {number[][]} line LineString coordinates (2+ points) + * @returns {Array<{ point: number[], ringSegIdx: number, ringT: number, lineSegIdx: number, lineT: number }>} + */ +function findIntersections(ring, line) { + const hits = []; + const eps = 1e-10; + + for (let li = 0; li < line.length - 1; li++) { + for (let ri = 0; ri < ring.length - 1; ri++) { + const ix = segmentIntersection(ring[ri], ring[ri + 1], line[li], line[li + 1], eps); + if (!ix) continue; + + // Skip if intersection is at the very start of the ring segment + // but was already caught as the end of the previous segment + const pt = ix.point; + + // Avoid duplicate hits at shared vertices + let isDup = false; + for (const h of hits) { + if (dist2(h.point, pt) < 1e-6) { + isDup = true; + break; + } + } + if (isDup) continue; + + hits.push({ + point: pt, + ringSegIdx: ri, + ringT: ix.t, + lineSegIdx: li, + lineT: ix.u, + }); + } + } + + // Sort by position along the cutting line + hits.sort((a, b) => { + if (a.lineSegIdx !== b.lineSegIdx) return a.lineSegIdx - b.lineSegIdx; + return a.lineT - b.lineT; + }); + + return hits; +} + +/** + * Insert intersection points into a ring, returning the expanded ring + * and the new indices of the inserted points. + * + * @param {number[][]} ring Closed ring (first === last) + * @param {Array<{ point: number[], ringSegIdx: number, ringT: number }>} hits + * Sorted by ringSegIdx then ringT. + * @returns {{ ring: number[][], indices: number[] }} + */ +function insertPointsIntoRing(ring, hits) { + // Sort hits by ring position (segment index, then parametric t) so + // we can insert from back to front without shifting earlier indices. + const sorted = hits.map((h, i) => ({ ...h, origOrder: i })); + sorted.sort((a, b) => { + if (a.ringSegIdx !== b.ringSegIdx) return a.ringSegIdx - b.ringSegIdx; + return a.ringT - b.ringT; + }); + + const expanded = ring.slice(); // copy + const indices = new Array(sorted.length); + + // Insert from the end so that earlier insertions don't shift later indices. + for (let k = sorted.length - 1; k >= 0; k--) { + const h = sorted[k]; + const insertIdx = h.ringSegIdx + 1; + + // Check if this point is essentially identical to an existing vertex + const snapDist = 1e-6; + if (dist2(h.point, expanded[h.ringSegIdx]) < snapDist) { + indices[h.origOrder] = h.ringSegIdx; + continue; + } + if (dist2(h.point, expanded[h.ringSegIdx + 1]) < snapDist) { + indices[h.origOrder] = h.ringSegIdx + 1; + continue; + } + + // Insert the new point + expanded.splice(insertIdx, 0, h.point); + indices[h.origOrder] = insertIdx; + + // Adjust indices for all previously recorded insertions + // that reference a position >= insertIdx + for (let j = k + 1; j < sorted.length; j++) { + if (indices[sorted[j].origOrder] >= insertIdx) { + indices[sorted[j].origOrder]++; + } + } + } + + return { ring: expanded, indices }; +} + +/** + * Extract a slice of a ring from index i0 to i1 (going forward, wrapping). + * Both endpoints are included. + * + * @param {number[][]} ring Closed ring (first === last); length includes closing vertex + * @param {number} i0 Start index (inclusive) + * @param {number} i1 End index (inclusive) + * @returns {number[][]} + */ +function ringSlice(ring, i0, i1) { + const n = ring.length - 1; // number of unique vertices (ring is closed) + // Normalise indices into the [0, n-1] range + const start = ((i0 % n) + n) % n; + const end = ((i1 % n) + n) % n; + const result = []; + let idx = start; + while (true) { + result.push(ring[idx]); + if (idx === end) break; + idx = (idx + 1) % n; + } + return result; +} + +/** + * Extract the cutting line segment between two intersection points. + * + * @param {number[][]} line Full cutting line coordinates + * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit0 + * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit1 + * @returns {number[][]} Coordinates from hit0.point to hit1.point along the line + */ +function cuttingLineSlice(line, hit0, hit1) { + const result = [hit0.point]; + + // Include all intermediate line vertices between the two hit segments + const startSeg = hit0.lineSegIdx; + const endSeg = hit1.lineSegIdx; + + for (let i = startSeg + 1; i <= endSeg; i++) { + result.push(line[i]); + } + + // Add the end intersection point if it's not the same as the last vertex + if (dist2(result[result.length - 1], hit1.point) > 1e-10) { + result.push(hit1.point); + } + + return result; +} + +/** + * Ensure a ring has the desired winding order. + * @param {number[][]} ring Closed ring + * @param {boolean} ccw true for counter-clockwise + * @returns {number[][]} + */ +function ensureWinding(ring, ccw) { + const area = signedArea(ring); + if ((ccw && area < 0) || (!ccw && area > 0)) { + return ring.slice().reverse(); + } + return ring; +} + +/** + * Close a ring (ensure first === last). + */ +function closeRing(coords) { + if (coords.length < 2) return coords; + const first = coords[0]; + const last = coords[coords.length - 1]; + if (dist2(first, last) > 1e-10) { + return [...coords, first.slice()]; + } + return coords; +} + +/** + * Extend a cutting line so that both endpoints lie outside the polygon ring. + * If an endpoint is inside, we extend the first/last segment outward past the + * bounding box diagonal so it definitely exits. + * + * @param {number[][]} line Cutting line coordinates + * @param {number[][]} ring Closed polygon ring + * @returns {number[][]} Extended line (may be the original if already outside) + */ +function extendLineOutsideRing(line, ring) { + // Compute bounding-box diagonal for a generous extension distance + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const pt of ring) { + if (pt[0] < minX) minX = pt[0]; + if (pt[1] < minY) minY = pt[1]; + if (pt[0] > maxX) maxX = pt[0]; + if (pt[1] > maxY) maxY = pt[1]; + } + const diag = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2) || 1; + + const result = line.slice(); + + // Extend start if inside + if (pointInRing(result[0], ring)) { + const p0 = result[0]; + const p1 = result[1]; + const dx = p0[0] - p1[0]; + const dy = p0[1] - p1[1]; + const len = Math.sqrt(dx * dx + dy * dy) || 1; + const scale = diag * 2 / len; + result[0] = [p0[0] + dx * scale, p0[1] + dy * scale]; + } + + // Extend end if inside + const last = result.length - 1; + if (pointInRing(result[last], ring)) { + const pN = result[last]; + const pN1 = result[last - 1]; + const dx = pN[0] - pN1[0]; + const dy = pN[1] - pN1[1]; + const len = Math.sqrt(dx * dx + dy * dy) || 1; + const scale = diag * 2 / len; + result[last] = [pN[0] + dx * scale, pN[1] + dy * scale]; + } + + return result; +} + +/** + * Split a polygon by a cutting line. + * + * The cutting line can start or end inside the polygon — the algorithm will + * automatically extend it outward so it crosses the boundary at exactly 2 + * points. Multi-vertex cutting lines (with corners or approximated arcs) + * are fully supported. + * + * @param {number[][][]} polygonCoords Polygon coordinates: + * [exteriorRing, ...holeRings] where each ring is closed (first === last) + * @param {number[][]} lineCoords Cutting line coordinates (2+ points) + * @returns {number[][][][] | null} Two polygon coordinate arrays, or null if split failed + */ +export function splitPolygonByLine(polygonCoords, lineCoords) { + const exteriorRing = polygonCoords[0]; + const holes = polygonCoords.slice(1); + + // Extend the cutting line if its endpoints are inside the polygon + const extendedLine = extendLineOutsideRing(lineCoords, exteriorRing); + + // 1. Find intersections between cutting line and exterior ring + const hits = findIntersections(exteriorRing, extendedLine); + + // We need exactly 2 intersection points for a simple split + if (hits.length !== 2) { + console.warn(`[polygonSplit] Expected 2 intersections, got ${hits.length}`); + return null; + } + + const [hit0, hit1] = hits; + + // 2. Insert intersection points into the ring + const { ring: expandedRing, indices } = insertPointsIntoRing(exteriorRing, hits); + const idx0 = indices[0]; + const idx1 = indices[1]; + + // Ensure idx0 < idx1 for consistent traversal + const [iA, iB] = idx0 < idx1 ? [idx0, idx1] : [idx1, idx0]; + const [hitA, hitB] = idx0 < idx1 ? [hit0, hit1] : [hit1, hit0]; + + // 3. Get the cutting line segment between the two intersection points + const cutForward = idx0 < idx1 + ? cuttingLineSlice(extendedLine, hit0, hit1) + : cuttingLineSlice(extendedLine, hit1, hit0); + const cutReverse = cutForward.slice().reverse(); + + // 4. Build two polygon rings + // Ring A: walk ring from iA to iB (forward), then cutting line reversed back to iA + const sliceAB = ringSlice(expandedRing, iA, iB); + const ringA = closeRing([...sliceAB, ...cutReverse.slice(1)]); + + // Ring B: walk ring from iB to iA (wrapping), then cutting line forward back to iB + const sliceBA = ringSlice(expandedRing, iB, iA); + const ringB = closeRing([...sliceBA, ...cutForward.slice(1)]); + + // 5. Match winding order to original + const originalCCW = signedArea(exteriorRing) > 0; + const finalA = ensureWinding(ringA, originalCCW); + const finalB = ensureWinding(ringB, originalCCW); + + // 6. Build polygon coordinate arrays, assigning holes to the correct piece + const polyA = [finalA]; + const polyB = [finalB]; + + for (const hole of holes) { + // Use the centroid of the hole to determine containment + const centroid = holeCentroid(hole); + if (pointInRing(centroid, finalA)) { + polyA.push(hole); + } else { + polyB.push(hole); + } + } + + return [polyA, polyB]; +} + +/** + * Compute the centroid of a closed ring. + */ +function holeCentroid(ring) { + let cx = 0, cy = 0; + const n = ring.length - 1; // exclude closing vertex + for (let i = 0; i < n; i++) { + cx += ring[i][0]; + cy += ring[i][1]; + } + return [cx / n, cy / n]; +} diff --git a/src/interactions/PolygonDivideInteraction.js b/src/interactions/PolygonDivideInteraction.js new file mode 100644 index 0000000..7a48bc5 --- /dev/null +++ b/src/interactions/PolygonDivideInteraction.js @@ -0,0 +1,492 @@ +/** + * PolygonDivideInteraction + * + * A three-phase OpenLayers interaction for dividing a polygon into N + * equal-area pieces: + * Phase 1 – SELECT: hover to highlight, click to select a polygon + * Phase 2 – EDGE: hover to highlight edges, click to pick the divide + * direction (cuts will be perpendicular to this edge) + * Phase 3 – FORM: wait for the popup form to call performDivide(n) + * + * After a successful divide the original feature is removed and N new + * coloured features are added. The interaction fires `beforedivide` and + * `afterdivide` events compatible with ol-ext's UndoRedo. + */ + +import ol_interaction_Interaction from 'ol/interaction/Interaction'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import Feature from 'ol/Feature'; +import { Style, Stroke, Fill } from 'ol/style'; +import { LineString, Polygon as PolygonGeom } from 'ol/geom'; +import { dividePolygon } from '../geom/polygonDivide.js'; +import { showToast } from '../toast.js'; + +// Highlight style for the selected polygon (phase 1) +const HIGHLIGHT_STYLE = new Style({ + stroke: new Stroke({ color: '#0ea5e9', width: 3 }), + fill: new Fill({ color: 'rgba(14,165,233,0.15)' }), +}); + +// Style for the hovered edge (phase 2) +const EDGE_STYLE = new Style({ + stroke: new Stroke({ color: '#8b5cf6', width: 4, lineDash: [10, 6] }), +}); + +/** + * Generate N visually distinct colours using evenly-spaced HSL hues. + */ +function pieceColors(n) { + const colors = []; + for (let i = 0; i < n; i++) { + const hue = Math.round((i * 360) / n); + colors.push({ + stroke: `hsl(${hue}, 70%, 45%)`, + fill: `hsla(${hue}, 70%, 55%, 0.25)`, + }); + } + return colors; +} + +export class PolygonDivideInteraction extends ol_interaction_Interaction { + /** + * @param {Object} options + * @param {VectorSource|VectorSource[]} [options.sources] Specific sources + * to search. If omitted the interaction searches all visible vector layers. + * @param {number} [options.snapDistance=25] Pixel distance for hover. + */ + constructor(options = {}) { + super({ + handleEvent: (e) => this._handleEvent(e), + }); + + this.snapDistance_ = options.snapDistance || 25; + this._sources = options.sources + ? (Array.isArray(options.sources) ? options.sources : [options.sources]) + : null; + + // Phase: 'select' | 'edge' | 'form' | 'pick' + this._phase = 'select'; + this._selectedFeature = null; + this._selectedSource = null; + this._selectedEdge = null; // [p0, p1] — the edge the user clicked + this._dividedFeatures = null; // features created after divide (for pick phase) + + // Overlay layer for polygon highlight + this._overlaySource = new VectorSource({ useSpatialIndex: false }); + this._overlayLayer = new VectorLayer({ + source: this._overlaySource, + displayInLayerSwitcher: false, + style: HIGHLIGHT_STYLE, + }); + + // Overlay layer for edge highlight + this._edgeSource = new VectorSource({ useSpatialIndex: false }); + this._edgeLayer = new VectorLayer({ + source: this._edgeSource, + displayInLayerSwitcher: false, + style: EDGE_STYLE, + }); + } + + /* ------------------------------------------------------------------ */ + /* Map lifecycle */ + /* ------------------------------------------------------------------ */ + + setMap(map) { + if (this.getMap()) { + this.getMap().removeLayer(this._overlayLayer); + this.getMap().removeLayer(this._edgeLayer); + } + super.setMap(map); + if (map) { + this._overlayLayer.setMap(map); + this._edgeLayer.setMap(map); + } + } + + setActive(active) { + super.setActive(active); + if (!active) { + this._reset(); + } + } + + /* ------------------------------------------------------------------ */ + /* Source helpers */ + /* ------------------------------------------------------------------ */ + + _getSources() { + if (this._sources) return this._sources; + if (!this.getMap()) return []; + const sources = []; + const collect = (layers) => { + layers.forEach((layer) => { + if (layer.getVisible()) { + if (layer.getSource && layer.getSource() instanceof VectorSource) { + sources.push(layer.getSource()); + } else if (layer.getLayers) { + collect(layer.getLayers()); + } + } + }); + }; + collect(this.getMap().getLayers()); + return sources; + } + + /* ------------------------------------------------------------------ */ + /* Event router */ + /* ------------------------------------------------------------------ */ + + _handleEvent(e) { + if (!this.getActive()) return true; + + // Escape cancels at any phase + if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') { + if (this._phase === 'form') { + this.cancelDivide(); + } else { + this._reset(); + } + return false; + } + + if (this._phase === 'select') { + if (e.type === 'pointermove') return this._onSelectMove(e); + if (e.type === 'singleclick') return this._onSelectClick(e); + } + + if (this._phase === 'edge') { + if (e.type === 'pointermove') return this._onEdgeMove(e); + if (e.type === 'singleclick') return this._onEdgeClick(e); + } + + if (this._phase === 'pick') { + if (e.type === 'pointermove') return this._onPickMove(e); + if (e.type === 'singleclick') return this._onPickClick(e); + } + + return true; + } + + /* ------------------------------------------------------------------ */ + /* Phase 1: SELECT polygon */ + /* ------------------------------------------------------------------ */ + + _onSelectMove(e) { + const map = this.getMap(); + if (!map) return true; + + this._overlaySource.clear(); + + const hit = this._closestPolygon(e); + if (hit) { + const clone = hit.feature.clone(); + this._overlaySource.addFeature(clone); + map.getTargetElement().style.cursor = 'pointer'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onSelectClick(e) { + const hit = this._closestPolygon(e); + if (!hit) return true; + + this._selectedFeature = hit.feature; + this._selectedSource = hit.source; + + // Keep polygon highlight visible during edge phase + this._overlaySource.clear(); + const clone = hit.feature.clone(); + clone.set('_permanent', true); + this._overlaySource.addFeature(clone); + + this._phase = 'edge'; + showToast('Click the edge to divide along.', 'info', 3000); + return false; + } + + _closestPolygon(e) { + let best = null; + let bestDist = this.snapDistance_ + 1; + + for (const source of this._getSources()) { + const feat = source.getClosestFeatureToCoordinate(e.coordinate); + if (!feat) continue; + const geom = feat.getGeometry(); + if (!geom) continue; + const type = geom.getType(); + if (type !== 'Polygon' && type !== 'MultiPolygon') continue; + + const closest = geom.getClosestPoint(e.coordinate); + const line = new LineString([e.coordinate, closest]); + const distPx = line.getLength() / e.frameState.viewState.resolution; + + if (distPx < bestDist) { + bestDist = distPx; + best = { feature: feat, source }; + } + } + return best; + } + + /* ------------------------------------------------------------------ */ + /* Phase 2: EDGE selection */ + /* ------------------------------------------------------------------ */ + + _onEdgeMove(e) { + const map = this.getMap(); + if (!map) return true; + + this._edgeSource.clear(); + + const edge = this._closestEdgeSegment(this._selectedFeature, e); + if (edge) { + const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd])); + this._edgeSource.addFeature(edgeFeat); + map.getTargetElement().style.cursor = 'crosshair'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onEdgeClick(e) { + const edge = this._closestEdgeSegment(this._selectedFeature, e); + if (!edge) return true; + + this._selectedEdge = [edge.segStart, edge.segEnd]; + this._edgeSource.clear(); + + this._phase = 'form'; + + // Dispatch divideform so MapView can show the popup + const geom = this._selectedFeature.getGeometry(); + const ext = geom.getExtent(); + const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2]; + + this.dispatchEvent({ + type: 'divideform', + feature: this._selectedFeature, + source: this._selectedSource, + coordinate: center, + }); + + return false; + } + + /** + * Find the closest edge segment of a polygon feature to the cursor. + */ + _closestEdgeSegment(feature, e) { + const geom = feature.getGeometry(); + let ring; + if (geom.getType() === 'Polygon') { + ring = geom.getCoordinates()[0]; + } else if (geom.getType() === 'MultiPolygon') { + ring = geom.getCoordinates()[0][0]; + } else { + return null; + } + + const resolution = e.frameState.viewState.resolution; + let bestDist = Infinity; + let bestSeg = null; + const n = ring.length - 1; + + for (let i = 0; i < n; i++) { + const a = ring[i]; + const b = ring[i + 1]; + const dx = b[0] - a[0], dy = b[1] - a[1]; + const lenSq = dx * dx + dy * dy; + if (lenSq < 1e-20) continue; + + let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq; + t = Math.max(0, Math.min(1, t)); + const projX = a[0] + t * dx, projY = a[1] + t * dy; + const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution; + + if (distPx < bestDist) { + bestDist = distPx; + bestSeg = { segStart: a, segEnd: b }; + } + } + return bestDist <= this.snapDistance_ ? bestSeg : null; + } + + /* ------------------------------------------------------------------ */ + /* Phase 3: FORM — called externally by the popup */ + /* ------------------------------------------------------------------ */ + + /** + * Divide the selected polygon into `n` equal-area pieces. + * Called by the MapView popup's Confirm handler. + * + * @param {number} n Number of pieces (>= 2) + */ + performDivide(n) { + if (this._phase !== 'form' || !this._selectedFeature) return; + + const feature = this._selectedFeature; + const source = this._selectedSource; + const geom = feature.getGeometry(); + + let polygonCoords; + if (geom.getType() === 'Polygon') { + polygonCoords = geom.getCoordinates(); + } else if (geom.getType() === 'MultiPolygon') { + polygonCoords = geom.getCoordinates()[0]; + } + + const result = dividePolygon(polygonCoords, n, this._selectedEdge); + + if (!result.pieces) { + showToast(result.error || 'Division failed.', 'error', 5000); + this._reset(); + return; + } + + // Create N new coloured features + const colors = pieceColors(n); + const newFeatures = result.pieces.map((coords, i) => { + const f = feature.clone(); + f.setGeometry(new PolygonGeom(coords)); + f.setStyle(new Style({ + stroke: new Stroke({ color: colors[i].stroke, width: 2.5 }), + fill: new Fill({ color: colors[i].fill }), + })); + return f; + }); + + // Dispatch beforedivide (UndoRedo compatible) + const evtData = { + type: 'beforedivide', + original: feature, + features: newFeatures, + }; + this.dispatchEvent(evtData); + source.dispatchEvent({ ...evtData }); + + // Replace original with pieces + source.removeFeature(feature); + for (const f of newFeatures) { + source.addFeature(f); + } + + // Dispatch afterdivide + const afterEvt = { + type: 'afterdivide', + original: feature, + features: newFeatures, + }; + this.dispatchEvent(afterEvt); + source.dispatchEvent({ ...afterEvt }); + + // If original was a parcel, enter pick phase for UPN assignment + const isParcel = feature.get('_layerType') === 'parcel'; + if (isParcel) { + this._dividedFeatures = newFeatures; + this._phase = 'pick'; + showToast('Click the polygon that should keep the original identifier.', 'info', 5000); + + this.dispatchEvent({ + type: 'dividedparcel', + features: newFeatures, + originalProps: feature.getProperties(), + source, + }); + } else { + showToast(`Polygon divided into ${n} equal pieces.`, 'success'); + this._reset(); + } + } + + /* ------------------------------------------------------------------ */ + /* Phase 4: PICK — select which piece keeps the UPN */ + /* ------------------------------------------------------------------ */ + + _onPickMove(e) { + const map = this.getMap(); + if (!map) return true; + + this._overlaySource.clear(); + + // Highlight whichever divided piece is under the cursor + const hit = this._closestDividedPiece(e); + if (hit) { + const clone = hit.clone(); + this._overlaySource.addFeature(clone); + map.getTargetElement().style.cursor = 'pointer'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onPickClick(e) { + const hit = this._closestDividedPiece(e); + if (!hit) return true; + + this.dispatchEvent({ + type: 'dividepick', + picked: hit, + features: this._dividedFeatures, + }); + + this._reset(); + return false; + } + + /** + * Find the closest divided piece to the cursor. + */ + _closestDividedPiece(e) { + if (!this._dividedFeatures) return null; + let best = null; + let bestDist = this.snapDistance_ + 1; + + for (const feat of this._dividedFeatures) { + const geom = feat.getGeometry(); + if (!geom) continue; + const closest = geom.getClosestPoint(e.coordinate); + const line = new LineString([e.coordinate, closest]); + const distPx = line.getLength() / e.frameState.viewState.resolution; + if (distPx < bestDist) { + bestDist = distPx; + best = feat; + } + } + return best; + } + + /** + * Cancel the divide operation and return to select phase. + * Called by the MapView popup's Cancel handler. + */ + cancelDivide() { + this.dispatchEvent({ type: 'dividecancel' }); + this._reset(); + } + + /* ------------------------------------------------------------------ */ + /* Reset */ + /* ------------------------------------------------------------------ */ + + _reset() { + this._phase = 'select'; + this._selectedFeature = null; + this._selectedSource = null; + this._selectedEdge = null; + this._dividedFeatures = null; + this._overlaySource.clear(); + this._edgeSource.clear(); + + const map = this.getMap(); + if (map) { + map.getTargetElement().style.cursor = ''; + } + } +} diff --git a/src/interactions/PolygonMergeInteraction.js b/src/interactions/PolygonMergeInteraction.js new file mode 100644 index 0000000..0da7443 --- /dev/null +++ b/src/interactions/PolygonMergeInteraction.js @@ -0,0 +1,475 @@ +/** + * PolygonMergeInteraction + * + * A four-phase OpenLayers interaction for merging two adjacent polygons: + * Phase 1 – SELECT_A: hover to highlight, click to select polygon A + * Phase 2 – SELECT_B: hover to highlight, click to select polygon B + * Phase 3 – CLICK_EDGE_A: hover highlights edge, click to pick shared edge on A + * Phase 4 – CLICK_EDGE_B: hover highlights edge, click to pick shared edge on B → merge + * + * After a successful merge the two original features are removed and one + * merged feature (coloured green) is added. If both originals were parcels, + * a `mergedparcel` event is fired so external code can present a UPN chooser. + */ + +import ol_interaction_Interaction from 'ol/interaction/Interaction'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import Feature from 'ol/Feature'; +import { Style, Stroke, Fill, Text } from 'ol/style'; +import { LineString, Polygon as PolygonGeom } from 'ol/geom'; +import { mergePolygons } from '../geom/polygonMerge.js'; +import { showToast } from '../toast.js'; + +// ── Styles ─────────────────────────────────────────────────────────────────── + +const HIGHLIGHT_A = new Style({ + stroke: new Stroke({ color: '#0ea5e9', width: 3 }), + fill: new Fill({ color: 'rgba(14,165,233,0.15)' }), +}); + +const HIGHLIGHT_B = new Style({ + stroke: new Stroke({ color: '#f59e0b', width: 3 }), + fill: new Fill({ color: 'rgba(245,158,11,0.15)' }), +}); + +// Labelled versions for permanent highlights (shown after selection) +const LABEL_A = new Style({ + stroke: new Stroke({ color: '#0ea5e9', width: 3 }), + fill: new Fill({ color: 'rgba(14,165,233,0.15)' }), + text: new Text({ + text: 'A', + font: 'bold 22px Exo, sans-serif', + fill: new Fill({ color: '#0ea5e9' }), + stroke: new Stroke({ color: '#fff', width: 4 }), + overflow: true, + }), +}); + +const LABEL_B = new Style({ + stroke: new Stroke({ color: '#f59e0b', width: 3 }), + fill: new Fill({ color: 'rgba(245,158,11,0.15)' }), + text: new Text({ + text: 'B', + font: 'bold 22px Exo, sans-serif', + fill: new Fill({ color: '#f59e0b' }), + stroke: new Stroke({ color: '#fff', width: 4 }), + overflow: true, + }), +}); + +const EDGE_STYLE = new Style({ + stroke: new Stroke({ color: '#ec4899', width: 4, lineDash: [10, 6] }), +}); + +const MERGE_STYLE = new Style({ + stroke: new Stroke({ color: '#10b981', width: 2.5 }), + fill: new Fill({ color: 'rgba(16,185,129,0.3)' }), +}); + +// ── Interaction ────────────────────────────────────────────────────────────── + +export class PolygonMergeInteraction extends ol_interaction_Interaction { + /** + * @param {Object} [options] + * @param {number} [options.snapDistance=25] Pixel distance for hover detection. + * @param {number} [options.tolerance=5] Map-unit tolerance for shared-edge matching. + */ + constructor(options = {}) { + super({ + handleEvent: (e) => this._handleEvent(e), + }); + + this.snapDistance_ = options.snapDistance || 25; + this.tolerance_ = options.tolerance || 5; + + // Phase: 'select_a' | 'select_b' | 'click_edge_a' | 'click_edge_b' + this._phase = 'select_a'; + + // Selected features and their sources + this._featureA = null; + this._sourceA = null; + this._featureB = null; + this._sourceB = null; + + // Clicked edge coordinates (map coords) + this._edgeClickA = null; + this._edgeClickB = null; + + // Overlay for polygon highlights + this._highlightSource = new VectorSource({ useSpatialIndex: false }); + this._highlightLayer = new VectorLayer({ + source: this._highlightSource, + displayInLayerSwitcher: false, + style: (f) => f.get('_highlightStyle') || HIGHLIGHT_A, + }); + + // Overlay for edge highlights + this._edgeSource = new VectorSource({ useSpatialIndex: false }); + this._edgeLayer = new VectorLayer({ + source: this._edgeSource, + displayInLayerSwitcher: false, + style: EDGE_STYLE, + }); + } + + /* ------------------------------------------------------------------ */ + /* Map lifecycle */ + /* ------------------------------------------------------------------ */ + + setMap(map) { + if (this.getMap()) { + this.getMap().removeLayer(this._highlightLayer); + this.getMap().removeLayer(this._edgeLayer); + } + super.setMap(map); + if (map) { + this._highlightLayer.setMap(map); + this._edgeLayer.setMap(map); + } + } + + setActive(active) { + super.setActive(active); + if (!active) this._reset(); + } + + /* ------------------------------------------------------------------ */ + /* Source helpers */ + /* ------------------------------------------------------------------ */ + + _getSources() { + if (!this.getMap()) return []; + const sources = []; + const collect = (layers) => { + layers.forEach((layer) => { + if (layer.getVisible()) { + if (layer.getSource && layer.getSource() instanceof VectorSource) { + sources.push(layer.getSource()); + } else if (layer.getLayers) { + collect(layer.getLayers()); + } + } + }); + }; + collect(this.getMap().getLayers()); + return sources; + } + + /* ------------------------------------------------------------------ */ + /* Event router */ + /* ------------------------------------------------------------------ */ + + _handleEvent(e) { + if (!this.getActive()) return true; + + // Escape cancels at any phase + if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') { + this._reset(); + return false; + } + + switch (this._phase) { + case 'select_a': + if (e.type === 'pointermove') return this._onSelectMove(e, null); + if (e.type === 'singleclick') return this._onSelectAClick(e); + break; + case 'select_b': + if (e.type === 'pointermove') return this._onSelectMove(e, this._featureA); + if (e.type === 'singleclick') return this._onSelectBClick(e); + break; + case 'click_edge_a': + if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureA); + if (e.type === 'singleclick') return this._onEdgeAClick(e); + break; + case 'click_edge_b': + if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureB); + if (e.type === 'singleclick') return this._onEdgeBClick(e); + break; + } + return true; + } + + /* ------------------------------------------------------------------ */ + /* Phase 1 & 2: SELECT polygons */ + /* ------------------------------------------------------------------ */ + + _onSelectMove(e, skipFeature) { + const map = this.getMap(); + if (!map) return true; + + // Keep existing highlights for already-selected polygons + this._highlightSource.clear(); + this._edgeSource.clear(); + this._rebuildHighlights(); + + const hit = this._closestPolygon(e, skipFeature); + if (hit) { + const style = this._phase === 'select_a' ? HIGHLIGHT_A : HIGHLIGHT_B; + const clone = hit.feature.clone(); + clone.set('_highlightStyle', style); + this._highlightSource.addFeature(clone); + map.getTargetElement().style.cursor = 'pointer'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onSelectAClick(e) { + const hit = this._closestPolygon(e, null); + if (!hit) return true; + + this._featureA = hit.feature; + this._sourceA = hit.source; + this._phase = 'select_b'; + + this._rebuildHighlights(); + return false; + } + + _onSelectBClick(e) { + const hit = this._closestPolygon(e, this._featureA); + if (!hit) return true; + + this._featureB = hit.feature; + this._sourceB = hit.source; + this._phase = 'click_edge_a'; + + this._rebuildHighlights(); + this.getMap().getTargetElement().style.cursor = 'crosshair'; + return false; + } + + /** + * Find the closest polygon feature within snap distance. + * Optionally skip a feature (used in phase 2 to avoid re-selecting A). + */ + _closestPolygon(e, skipFeature) { + let best = null; + let bestDist = this.snapDistance_ + 1; + + for (const source of this._getSources()) { + const feat = source.getClosestFeatureToCoordinate(e.coordinate); + if (!feat) continue; + if (skipFeature && feat === skipFeature) continue; + const geom = feat.getGeometry(); + if (!geom) continue; + const type = geom.getType(); + if (type !== 'Polygon' && type !== 'MultiPolygon') continue; + + const closest = geom.getClosestPoint(e.coordinate); + const line = new LineString([e.coordinate, closest]); + const distPx = line.getLength() / e.frameState.viewState.resolution; + + if (distPx < bestDist) { + bestDist = distPx; + best = { feature: feat, source, coord: closest }; + } + } + return best; + } + + /* ------------------------------------------------------------------ */ + /* Phase 3 & 4: CLICK edges */ + /* ------------------------------------------------------------------ */ + + _onEdgeMove(e, feature) { + const map = this.getMap(); + if (!map) return true; + + this._edgeSource.clear(); + + const edge = this._closestEdgeSegment(feature, e); + if (edge) { + const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd])); + this._edgeSource.addFeature(edgeFeat); + map.getTargetElement().style.cursor = 'crosshair'; + } + return true; + } + + _onEdgeAClick(e) { + this._edgeClickA = e.coordinate; + this._phase = 'click_edge_b'; + this._edgeSource.clear(); + return false; + } + + _onEdgeBClick(e) { + this._edgeClickB = e.coordinate; + this._performMerge(); + return false; + } + + /** + * Find the closest edge segment of a polygon feature to the cursor. + */ + _closestEdgeSegment(feature, e) { + const geom = feature.getGeometry(); + let ring; + if (geom.getType() === 'Polygon') { + ring = geom.getCoordinates()[0]; + } else if (geom.getType() === 'MultiPolygon') { + ring = geom.getCoordinates()[0][0]; + } else { + return null; + } + + const resolution = e.frameState.viewState.resolution; + let bestDist = Infinity; + let bestSeg = null; + const n = ring.length - 1; + + for (let i = 0; i < n; i++) { + const a = ring[i]; + const b = ring[i + 1]; + const dx = b[0] - a[0], dy = b[1] - a[1]; + const lenSq = dx * dx + dy * dy; + if (lenSq < 1e-20) continue; + + let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq; + t = Math.max(0, Math.min(1, t)); + const projX = a[0] + t * dx, projY = a[1] + t * dy; + const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution; + + if (distPx < bestDist) { + bestDist = distPx; + bestSeg = { segStart: a, segEnd: b }; + } + } + return bestDist <= this.snapDistance_ ? bestSeg : null; + } + + /* ------------------------------------------------------------------ */ + /* Merge logic */ + /* ------------------------------------------------------------------ */ + + _performMerge() { + const featureA = this._featureA; + const featureB = this._featureB; + const sourceA = this._sourceA; + const sourceB = this._sourceB; + + // Extract polygon coordinates + const geomA = featureA.getGeometry(); + const geomB = featureB.getGeometry(); + const coordsA = geomA.getType() === 'Polygon' ? geomA.getCoordinates() : geomA.getCoordinates()[0]; + const coordsB = geomB.getType() === 'Polygon' ? geomB.getCoordinates() : geomB.getCoordinates()[0]; + + const result = mergePolygons(coordsA, coordsB, this._edgeClickA, this._edgeClickB, this.tolerance_); + + if (!result.coords) { + showToast(result.error || 'Merge failed — try clicking on the shared boundary.', 'error', 5000); + // Return to edge click phase for retry + this._edgeClickA = null; + this._edgeClickB = null; + this._phase = 'click_edge_a'; + this._edgeSource.clear(); + return; + } + + // Create merged feature (clone A for default properties) + const mergedFeature = featureA.clone(); + mergedFeature.setGeometry(new PolygonGeom(result.coords)); + mergedFeature.setStyle(MERGE_STYLE); + + // Dispatch beforemerge events + const evtData = { + type: 'beforemerge', + original: [featureA, featureB], + merged: mergedFeature, + }; + this.dispatchEvent(evtData); + sourceA.dispatchEvent({ ...evtData }); + if (sourceB !== sourceA) { + sourceB.dispatchEvent({ ...evtData }); + } + + // Replace originals with merged + sourceA.removeFeature(featureA); + sourceB.removeFeature(featureB); + sourceA.addFeature(mergedFeature); + + // Dispatch aftermerge events + const afterEvt = { + type: 'aftermerge', + original: [featureA, featureB], + merged: mergedFeature, + }; + this.dispatchEvent(afterEvt); + sourceA.dispatchEvent({ ...afterEvt }); + if (sourceB !== sourceA) { + sourceB.dispatchEvent({ ...afterEvt }); + } + + // If both features were parcels, fire mergedparcel so MapView can show the UPN chooser + const isParcelA = featureA.get('_layerType') === 'parcel'; + const isParcelB = featureB.get('_layerType') === 'parcel'; + if (isParcelA && isParcelB) { + this.dispatchEvent({ + type: 'mergedparcel', + merged: mergedFeature, + propsA: featureA.getProperties(), + propsB: featureB.getProperties(), + coordinate: this._edgeClickA, + }); + showToast('Polygons merged — choose which identifier to keep.', 'success'); + } else { + showToast('Polygons merged successfully.', 'success'); + } + + // Clean up + this._reset(); + } + + /* ------------------------------------------------------------------ */ + /* Highlight management */ + /* ------------------------------------------------------------------ */ + + /** + * Rebuild the permanent highlights for already-selected polygons. + */ + _rebuildHighlights() { + // Remove previous non-hover highlights + const toRemove = []; + this._highlightSource.getFeatures().forEach((f) => { + if (f.get('_permanent')) toRemove.push(f); + }); + toRemove.forEach((f) => this._highlightSource.removeFeature(f)); + + if (this._featureA) { + const cloneA = this._featureA.clone(); + cloneA.set('_highlightStyle', LABEL_A); + cloneA.set('_permanent', true); + this._highlightSource.addFeature(cloneA); + } + if (this._featureB) { + const cloneB = this._featureB.clone(); + cloneB.set('_highlightStyle', LABEL_B); + cloneB.set('_permanent', true); + this._highlightSource.addFeature(cloneB); + } + } + + /* ------------------------------------------------------------------ */ + /* Reset */ + /* ------------------------------------------------------------------ */ + + _reset() { + this._phase = 'select_a'; + this._featureA = null; + this._sourceA = null; + this._featureB = null; + this._sourceB = null; + this._edgeClickA = null; + this._edgeClickB = null; + this._highlightSource.clear(); + this._edgeSource.clear(); + + const map = this.getMap(); + if (map) { + map.getTargetElement().style.cursor = ''; + } + } +} diff --git a/src/interactions/PolygonSplitInteraction.js b/src/interactions/PolygonSplitInteraction.js new file mode 100644 index 0000000..3284669 --- /dev/null +++ b/src/interactions/PolygonSplitInteraction.js @@ -0,0 +1,433 @@ +/** + * PolygonSplitInteraction + * + * A two-phase OpenLayers interaction for splitting polygons: + * Phase 1 – SELECT: hover to highlight, click to select a polygon + * Phase 2 – DRAW: draw a cutting line, double-click to finish + * + * After a successful split the original feature is removed and two new + * coloured features are added. The interaction fires `beforesplit` and + * `aftersplit` events compatible with ol-ext's UndoRedo. + */ + +import ol_interaction_Interaction from 'ol/interaction/Interaction'; +import ol_interaction_Draw from 'ol/interaction/Draw'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import Feature from 'ol/Feature'; +import { Style, Stroke, Fill, Circle as CircleStyle } from 'ol/style'; +import { LineString } from 'ol/geom'; +import { Polygon as PolygonGeom } from 'ol/geom'; +import { splitPolygonByLine } from '../geom/polygonSplit.js'; +import { showToast } from '../toast.js'; + +// Marker colours for the two split pieces +const SPLIT_COLORS = [ + { stroke: '#ef4444', fill: 'rgba(239,68,68,0.25)' }, // red + { stroke: '#3b82f6', fill: 'rgba(59,130,246,0.25)' }, // blue +]; + +// Highlight style for the selected polygon (phase 1) +const HIGHLIGHT_STYLE = new Style({ + stroke: new Stroke({ color: '#0ea5e9', width: 3 }), + fill: new Fill({ color: 'rgba(14,165,233,0.15)' }), +}); + +// Style for the cutting-line sketch (phase 2) +const SKETCH_STYLE = new Style({ + stroke: new Stroke({ color: '#f43f5e', width: 2, lineDash: [8, 6] }), + image: new CircleStyle({ + radius: 5, + fill: new Fill({ color: '#f43f5e' }), + stroke: new Stroke({ color: '#fff', width: 1.5 }), + }), +}); + +export class PolygonSplitInteraction extends ol_interaction_Interaction { + /** + * @param {Object} options + * @param {VectorSource|VectorSource[]} [options.sources] Sources containing + * polygons to split. If omitted the interaction searches all visible + * vector layers on the map. + * @param {number} [options.snapDistance=25] Pixel distance for hover highlight. + */ + constructor(options = {}) { + super({ + handleEvent: (e) => this._handleEvent(e), + }); + + this.snapDistance_ = options.snapDistance || 25; + this._sources = options.sources + ? (Array.isArray(options.sources) ? options.sources : [options.sources]) + : null; + + // Phase: 'select' | 'draw' | 'pick' + this._phase = 'select'; + this._selectedFeature = null; + this._selectedSource = null; + this._drawInteraction = null; + this._splitFeatures = null; // the two pieces (for pick phase) + + // Overlay layer for highlighting the polygon under the cursor / selected + this._overlaySource = new VectorSource({ useSpatialIndex: false }); + this._overlayLayer = new VectorLayer({ + source: this._overlaySource, + displayInLayerSwitcher: false, + style: HIGHLIGHT_STYLE, + }); + } + + /* ------------------------------------------------------------------ */ + /* Map lifecycle */ + /* ------------------------------------------------------------------ */ + + setMap(map) { + if (this.getMap()) { + this.getMap().removeLayer(this._overlayLayer); + this._removeDrawInteraction(); + } + super.setMap(map); + if (map) { + this._overlayLayer.setMap(map); + } + } + + setActive(active) { + super.setActive(active); + if (!active) { + this._reset(); + } + } + + /* ------------------------------------------------------------------ */ + /* Source helpers */ + /* ------------------------------------------------------------------ */ + + _getSources() { + if (this._sources) return this._sources; + if (!this.getMap()) return []; + const sources = []; + const collect = (layers) => { + layers.forEach((layer) => { + if (layer.getVisible()) { + if (layer.getSource && layer.getSource() instanceof VectorSource) { + sources.push(layer.getSource()); + } else if (layer.getLayers) { + collect(layer.getLayers()); + } + } + }); + }; + collect(this.getMap().getLayers()); + return sources; + } + + /* ------------------------------------------------------------------ */ + /* Event router */ + /* ------------------------------------------------------------------ */ + + _handleEvent(e) { + if (!this.getActive()) return true; + + if (this._phase === 'select') { + if (e.type === 'pointermove') return this._onSelectMove(e); + if (e.type === 'singleclick') return this._onSelectClick(e); + } + // In 'draw' phase the Draw interaction handles events directly; + // we only intercept Escape to cancel. + if (this._phase === 'draw') { + if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') { + this._cancelDraw(); + return false; + } + } + + // In 'pick' phase the user selects which split piece keeps the UPN + if (this._phase === 'pick') { + if (e.type === 'pointermove') return this._onPickMove(e); + if (e.type === 'singleclick') return this._onPickClick(e); + if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') { + this._reset(); + return false; + } + } + + return true; + } + + /* ------------------------------------------------------------------ */ + /* Phase 1: SELECT */ + /* ------------------------------------------------------------------ */ + + _onSelectMove(e) { + const map = this.getMap(); + if (!map) return true; + + this._overlaySource.clear(); + + const hit = this._closestPolygon(e); + if (hit) { + // Show highlight copy + const clone = hit.feature.clone(); + this._overlaySource.addFeature(clone); + map.getTargetElement().style.cursor = 'pointer'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onSelectClick(e) { + const hit = this._closestPolygon(e); + if (!hit) return true; + + this._selectedFeature = hit.feature; + this._selectedSource = hit.source; + + // Keep highlight visible during draw phase + this._overlaySource.clear(); + const clone = hit.feature.clone(); + this._overlaySource.addFeature(clone); + + this._startDrawPhase(); + return false; // consume the click + } + + /** + * Find the closest polygon feature within snap distance. + */ + _closestPolygon(e) { + let best = null; + let bestDist = this.snapDistance_ + 1; + + for (const source of this._getSources()) { + const feat = source.getClosestFeatureToCoordinate(e.coordinate); + if (!feat) continue; + const geom = feat.getGeometry(); + if (!geom) continue; + const type = geom.getType(); + if (type !== 'Polygon' && type !== 'MultiPolygon') continue; + + const closest = geom.getClosestPoint(e.coordinate); + const line = new LineString([e.coordinate, closest]); + const distPx = line.getLength() / e.frameState.viewState.resolution; + + if (distPx < bestDist) { + bestDist = distPx; + best = { feature: feat, source, coord: closest }; + } + } + return best; + } + + /* ------------------------------------------------------------------ */ + /* Phase 2: DRAW cutting line */ + /* ------------------------------------------------------------------ */ + + _startDrawPhase() { + this._phase = 'draw'; + const map = this.getMap(); + if (!map) return; + + map.getTargetElement().style.cursor = 'crosshair'; + + this._drawInteraction = new ol_interaction_Draw({ + type: 'LineString', + style: SKETCH_STYLE, + }); + + this._drawInteraction.on('drawend', (evt) => { + const cuttingLine = evt.feature.getGeometry().getCoordinates(); + this._performSplit(cuttingLine); + }); + + map.addInteraction(this._drawInteraction); + } + + _removeDrawInteraction() { + if (this._drawInteraction && this.getMap()) { + this.getMap().removeInteraction(this._drawInteraction); + } + this._drawInteraction = null; + } + + _cancelDraw() { + this._removeDrawInteraction(); + this._reset(); + } + + /* ------------------------------------------------------------------ */ + /* Split logic */ + /* ------------------------------------------------------------------ */ + + _performSplit(cuttingLineCoords) { + const feature = this._selectedFeature; + const source = this._selectedSource; + const geom = feature.getGeometry(); + + let polygonCoords; + if (geom.getType() === 'Polygon') { + polygonCoords = geom.getCoordinates(); + } else if (geom.getType() === 'MultiPolygon') { + // For MultiPolygon, try to split each sub-polygon and use the + // first one that produces a valid result. + // For now, use the first polygon ring. + polygonCoords = geom.getCoordinates()[0]; + } + + const result = splitPolygonByLine(polygonCoords, cuttingLineCoords); + + if (!result) { + console.warn('[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points.'); + // Stay in draw phase so user can retry + this._removeDrawInteraction(); + this._startDrawPhase(); + return; + } + + const [coordsA, coordsB] = result; + + // Create two new features from the split result + const featureA = feature.clone(); + featureA.setGeometry(new PolygonGeom(coordsA)); + featureA.setStyle(new Style({ + stroke: new Stroke({ color: SPLIT_COLORS[0].stroke, width: 2.5 }), + fill: new Fill({ color: SPLIT_COLORS[0].fill }), + })); + + const featureB = feature.clone(); + featureB.setGeometry(new PolygonGeom(coordsB)); + featureB.setStyle(new Style({ + stroke: new Stroke({ color: SPLIT_COLORS[1].stroke, width: 2.5 }), + fill: new Fill({ color: SPLIT_COLORS[1].fill }), + })); + + // Dispatch beforesplit (compatible with ol-ext UndoRedo) + const splitFeatures = [featureA, featureB]; + this.dispatchEvent({ + type: 'beforesplit', + original: feature, + features: splitFeatures, + }); + source.dispatchEvent({ + type: 'beforesplit', + original: feature, + features: splitFeatures, + }); + + // Replace the original feature + source.removeFeature(feature); + source.addFeature(featureA); + source.addFeature(featureB); + + // Dispatch aftersplit + this.dispatchEvent({ + type: 'aftersplit', + original: feature, + features: splitFeatures, + }); + source.dispatchEvent({ + type: 'aftersplit', + original: feature, + features: splitFeatures, + }); + + // Clean up draw interaction + this._removeDrawInteraction(); + + // If the original was a parcel, enter pick phase for UPN assignment + const isParcel = feature.get('_layerType') === 'parcel'; + if (isParcel) { + this._splitFeatures = splitFeatures; + this._phase = 'pick'; + this._overlaySource.clear(); + const map = this.getMap(); + if (map) map.getTargetElement().style.cursor = ''; + showToast('Click the polygon that should keep the original identifier.', 'info', 5000); + + this.dispatchEvent({ + type: 'splitparcel', + features: splitFeatures, + originalProps: feature.getProperties(), + source, + }); + } else { + this._reset(); + } + } + + /* ------------------------------------------------------------------ */ + /* Phase 3: PICK — select which split piece keeps the UPN */ + /* ------------------------------------------------------------------ */ + + _onPickMove(e) { + const map = this.getMap(); + if (!map) return true; + + this._overlaySource.clear(); + + const hit = this._closestSplitPiece(e); + if (hit) { + const clone = hit.clone(); + this._overlaySource.addFeature(clone); + map.getTargetElement().style.cursor = 'pointer'; + } else { + map.getTargetElement().style.cursor = ''; + } + return true; + } + + _onPickClick(e) { + const hit = this._closestSplitPiece(e); + if (!hit) return true; + + this.dispatchEvent({ + type: 'splitpick', + picked: hit, + features: this._splitFeatures, + }); + + this._reset(); + return false; + } + + /** + * Find the closest split piece to the cursor. + */ + _closestSplitPiece(e) { + if (!this._splitFeatures) return null; + let best = null; + let bestDist = this.snapDistance_ + 1; + + for (const feat of this._splitFeatures) { + const geom = feat.getGeometry(); + if (!geom) continue; + const closest = geom.getClosestPoint(e.coordinate); + const line = new LineString([e.coordinate, closest]); + const distPx = line.getLength() / e.frameState.viewState.resolution; + if (distPx < bestDist) { + bestDist = distPx; + best = feat; + } + } + return best; + } + + /* ------------------------------------------------------------------ */ + /* Reset */ + /* ------------------------------------------------------------------ */ + + _reset() { + this._phase = 'select'; + this._selectedFeature = null; + this._selectedSource = null; + this._splitFeatures = null; + this._overlaySource.clear(); + this._removeDrawInteraction(); + + const map = this.getMap(); + if (map) { + map.getTargetElement().style.cursor = ''; + } + } +} diff --git a/src/offlineTiles.js b/src/offlineTiles.js new file mode 100644 index 0000000..b804765 --- /dev/null +++ b/src/offlineTiles.js @@ -0,0 +1,293 @@ +/** + * Offline Tile Downloader + * + * Pre-fetches map tiles for a given extent and zoom range so they are stored + * in the Service Worker's per-host tile cache for offline use. + * + * The downloader simply issues `fetch()` calls; the existing SW intercepts + * them and routes to the right cache bucket. No direct Cache API access is + * needed here — the SW is the single source of truth for storage. + * + * Throttling defaults are conservative to respect tile-server usage policies: + * • 2 concurrent requests + * • 50 ms inter-batch delay + * • Standard browser User-Agent / Referer headers + * + * Usage: + * const downloader = new OfflineTileDownloader({ + * baseMap: 'topo', + * extent3857: [minX, minY, maxX, maxY], // EPSG:3857 + * minZoom: 10, + * maxZoom: 15, + * onProgress: (s) => console.log(s), + * }); + * await downloader.start(); + * downloader.cancel(); // any time + */ + +// ============================================================================ +// Base-map URL templates +// ============================================================================ + +/** + * Tile URL templates for base maps that may be downloaded for offline use. + * + * The SW recognises these hosts in `getTileCacheName()` and routes them to + * the matching `tiles-*-vN` cache. If you add a new entry here, also add + * the host to the SW's classifier or the tiles will not be cached. + */ +export const BASEMAP_TEMPLATES = { + topo: { + url: 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png', + label: 'Topographic', + maxZoom: 17, + cacheKey: 'tiles-topo', + }, + osm: { + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + label: 'OpenStreetMap', + maxZoom: 19, + cacheKey: 'tiles-osm', + }, +}; + +// Approximate bytes per raster tile — used for storage estimates. +export const AVG_TILE_BYTES = 30 * 1024; + +// ============================================================================ +// Tile coordinate math (Web Mercator XYZ scheme) +// ============================================================================ + +const ORIGIN_SHIFT = 2 * Math.PI * 6378137 / 2; // 20037508.342789244 + +/** Convert Web Mercator metres → (lon, lat) in degrees. */ +function metersToLonLat(x, y) { + const lon = (x / ORIGIN_SHIFT) * 180; + let lat = (y / ORIGIN_SHIFT) * 180; + lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2); + return [lon, lat]; +} + +/** Tile (x, y) in XYZ scheme for a given lon/lat at zoom z. */ +function lonLatToTile(lon, lat, z) { + const n = Math.pow(2, z); + const x = Math.floor((lon + 180) / 360 * n); + const latRad = lat * Math.PI / 180; + const y = Math.floor( + (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n + ); + return { x, y }; +} + +/** Tile range covering an EPSG:3857 extent at a given zoom level. */ +export function tileRangeForExtent(extent3857, z) { + const [minX, minY, maxX, maxY] = extent3857; + const [minLon, minLat] = metersToLonLat(minX, minY); + const [maxLon, maxLat] = metersToLonLat(maxX, maxY); + + const tl = lonLatToTile(minLon, maxLat, z); // top-left in XYZ (NW) + const br = lonLatToTile(maxLon, minLat, z); // bottom-right (SE) + + const n = Math.pow(2, z); + const minTileX = Math.max(0, Math.min(tl.x, br.x)); + const maxTileX = Math.min(n - 1, Math.max(tl.x, br.x)); + const minTileY = Math.max(0, Math.min(tl.y, br.y)); + const maxTileY = Math.min(n - 1, Math.max(tl.y, br.y)); + + return { + z, + minX: minTileX, maxX: maxTileX, + minY: minTileY, maxY: maxTileY, + count: (maxTileX - minTileX + 1) * (maxTileY - minTileY + 1), + }; +} + +/** Total tile count for an extent across a zoom range (inclusive). */ +export function countTiles(extent3857, minZ, maxZ) { + let total = 0; + for (let z = minZ; z <= maxZ; z++) { + total += tileRangeForExtent(extent3857, z).count; + } + return total; +} + +/** + * Enumerate every tile in an extent across a zoom range. + * Returns an array of { z, x, y } objects. For very large ranges this can be + * large — the caller is expected to validate the count first. + */ +export function enumerateTiles(extent3857, minZ, maxZ) { + const out = []; + for (let z = minZ; z <= maxZ; z++) { + const r = tileRangeForExtent(extent3857, z); + for (let x = r.minX; x <= r.maxX; x++) { + for (let y = r.minY; y <= r.maxY; y++) { + out.push({ z, x, y }); + } + } + } + return out; +} + +/** + * Format a tile URL for a given coordinate using a {z}/{x}/{y} template. + */ +export function formatTileUrl(template, { z, x, y }) { + return template + .replace('{z}', z) + .replace('{x}', x) + .replace('{y}', y); +} + +// ============================================================================ +// OfflineTileDownloader +// ============================================================================ + +/** + * Concurrent, throttled tile downloader. Issues `fetch()` per tile; the + * service worker handles caching transparently. + * + * Events via `onProgress` callback: + * { phase: 'running' | 'done' | 'cancelled' | 'error', + * done, total, ok, failed, cached, + * elapsedMs, etaMs } + */ +export class OfflineTileDownloader { + constructor({ + baseMap, // 'topo' | 'osm' + extent3857, // [minX, minY, maxX, maxY] + minZoom, + maxZoom, + concurrency = 2, // OSM ToS-friendly default + interBatchDelayMs = 50, + onProgress = () => {}, + }) { + const tpl = BASEMAP_TEMPLATES[baseMap]; + if (!tpl) throw new Error(`Unknown base map: ${baseMap}`); + if (maxZoom > tpl.maxZoom) { + console.warn(`[OfflineTiles] ${baseMap}: maxZoom ${maxZoom} > supported ${tpl.maxZoom}; clamping`); + maxZoom = tpl.maxZoom; + } + + this.baseMap = baseMap; + this.template = tpl.url; + this.extent = extent3857; + this.minZoom = minZoom; + this.maxZoom = maxZoom; + this.concurrency = Math.max(1, Math.min(concurrency, 6)); + this.interBatchDelayMs = interBatchDelayMs; + this.onProgress = onProgress; + + this._abortCtrl = null; + this._cancelled = false; + } + + /** + * Begin downloading. Returns a Promise that resolves with the final stats + * when complete, or when cancelled. + */ + async start() { + if (this._abortCtrl) throw new Error('Downloader already started'); + this._abortCtrl = new AbortController(); + this._cancelled = false; + + const tiles = enumerateTiles(this.extent, this.minZoom, this.maxZoom); + const total = tiles.length; + const startedAt = Date.now(); + + let done = 0, ok = 0, failed = 0, cached = 0; + + const emit = (phase) => { + const elapsedMs = Date.now() - startedAt; + const etaMs = done > 0 ? Math.round((elapsedMs / done) * (total - done)) : null; + this.onProgress({ phase, done, total, ok, failed, cached, elapsedMs, etaMs }); + }; + + emit('running'); + + // Process in chunks of `concurrency` + for (let i = 0; i < tiles.length; i += this.concurrency) { + if (this._cancelled) break; + + const batch = tiles.slice(i, i + this.concurrency); + await Promise.all(batch.map(async (t) => { + if (this._cancelled) return; + const url = formatTileUrl(this.template, t); + + try { + const res = await fetch(url, { + signal: this._abortCtrl.signal, + // Hint the SW that this is a passive prefetch + cache: 'default', + }); + + if (res.ok) { + ok++; + // Detect "served from SW cache" via headers — not reliable across + // implementations, so we just count all 200s as ok. Reading the body + // (or cancelling it) lets the browser GC the response promptly. + if (res.body) res.body.cancel().catch(() => {}); + } else if (res.status === 408) { + // Our SW returns 408 when offline AND nothing cached. Treat as failed. + failed++; + } else { + failed++; + } + } catch (err) { + if (err.name === 'AbortError') { + // Cancellation — don't count + } else { + failed++; + } + } + done++; + })); + + emit('running'); + + if (this.interBatchDelayMs > 0 && i + this.concurrency < tiles.length) { + await new Promise((r) => setTimeout(r, this.interBatchDelayMs)); + } + } + + emit(this._cancelled ? 'cancelled' : 'done'); + + return { + phase: this._cancelled ? 'cancelled' : 'done', + done, total, ok, failed, cached, + elapsedMs: Date.now() - startedAt, + }; + } + + /** + * Cancel an in-flight download. Resolves on the next batch boundary. + */ + cancel() { + this._cancelled = true; + if (this._abortCtrl) this._abortCtrl.abort(); + } +} + +// ============================================================================ +// Predefined extents +// ============================================================================ + +/** + * Whole-of-Ghana bounding box in EPSG:3857. + * Approximate: -3.3°W → 1.2°E, 4.5°N → 11.2°N. + */ +export const GHANA_EXTENT_3857 = (() => { + const lonLatToMeters = (lon, lat) => { + const x = lon * ORIGIN_SHIFT / 180; + const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + return [x, y * ORIGIN_SHIFT / 180]; + }; + const sw = lonLatToMeters(-3.3, 4.5); + const ne = lonLatToMeters(1.2, 11.2); + return [sw[0], sw[1], ne[0], ne[1]]; +})(); + +// Useful for size estimates +export function estimatedSizeBytes(tileCount) { + return tileCount * AVG_TILE_BYTES; +} diff --git a/src/pdf-export.js b/src/pdf-export.js new file mode 100644 index 0000000..e1a21e4 --- /dev/null +++ b/src/pdf-export.js @@ -0,0 +1,157 @@ +/** + * PDF Export Module + * + * Generates branded PDF reports from analysis data. + * Uses jspdf + jspdf-autotable, loaded on demand via dynamic import + * from the calling code so the library is only fetched when needed. + * + * Usage: + * import { exportAnalysisPDF } from '../pdf-export.js'; + * await exportAnalysisPDF({ title: 'Circle Analysis', rows: [...] }); + */ + +import { jsPDF } from 'jspdf'; +import { applyPlugin } from 'jspdf-autotable'; + +applyPlugin(jsPDF); + +// Cached logo data URL — fetched once, reused for subsequent exports +let _logoDataUrl = null; + +/** + * Load the LUSPA logo, draw it onto a canvas to flatten alpha and + * produce a clean JPEG data URL that jsPDF can embed reliably. + * Caches the result so subsequent calls are instant. + */ +async function getLogoDataUrl() { + if (_logoDataUrl) return _logoDataUrl; + + try { + const img = new Image(); + img.crossOrigin = 'anonymous'; + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + img.src = './icons/luspa-pdf.jpg'; + }); + + // Draw onto a canvas to get a reliable JPEG data URL + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const ctx = canvas.getContext('2d'); + // White background to flatten any remaining alpha + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); + + _logoDataUrl = canvas.toDataURL('image/jpeg', 0.92); + return _logoDataUrl; + } catch (err) { + console.warn('[PDF] Could not load logo:', err); + return null; + } +} + +/** + * Generate and open a branded PDF report. + * + * @param {Object} options + * @param {string} options.title - Report title (e.g. "Circle Analysis") + * @param {Array<{label: string, value: string}>} options.rows - Table data + */ +export async function exportAnalysisPDF({ title, rows }) { + const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); + const pageWidth = doc.internal.pageSize.getWidth(); + + // Brand colours + const navy = [30, 26, 75]; // #1e1a4b + + // ---- Logo ---- + const logo = await getLogoDataUrl(); + const logoSize = 28; + const marginLeft = 14; + let cursorY = 14; + + if (logo) { + doc.addImage(logo, 'JPEG', marginLeft, cursorY, logoSize, logoSize); + } + + // ---- Header text (next to logo) ---- + const textX = marginLeft + logoSize + 6; + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(18); + doc.setTextColor(...navy); + doc.text('LUPMIS', textX, cursorY + 11); + + doc.setFont('helvetica', 'normal'); + doc.setFontSize(12); + doc.text(title, textX, cursorY + 19); + + // ---- Date / time ---- + const now = new Date(); + const dateStr = now.toLocaleDateString(undefined, { + year: 'numeric', month: 'long', day: 'numeric', + }); + const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + doc.setFontSize(9); + doc.setTextColor(120, 120, 120); + doc.text(`${dateStr} ${timeStr}`, pageWidth - marginLeft, cursorY + 11, { align: 'right' }); + + // ---- Divider line ---- + cursorY += logoSize + 6; + doc.setDrawColor(...navy); + doc.setLineWidth(0.5); + doc.line(marginLeft, cursorY, pageWidth - marginLeft, cursorY); + cursorY += 6; + + // ---- Analysis table ---- + const tableBody = rows.map(r => [r.label, r.value]); + + doc.autoTable({ + startY: cursorY, + head: [['Property', 'Value']], + body: tableBody, + margin: { left: marginLeft, right: marginLeft }, + styles: { + font: 'helvetica', + fontSize: 10, + cellPadding: 4, + }, + headStyles: { + fillColor: navy, + textColor: [255, 255, 255], + fontStyle: 'bold', + }, + alternateRowStyles: { + fillColor: [245, 245, 250], + }, + columnStyles: { + 0: { fontStyle: 'bold', cellWidth: 50 }, + }, + }); + + // ---- Footer ---- + const finalY = doc.lastAutoTable.finalY + 10; + doc.setFontSize(8); + doc.setTextColor(160, 160, 160); + doc.text('Generated by LUPMIS2 Land Use Planning & Management Information System', marginLeft, finalY); + + // ---- Open in browser ---- + const blob = doc.output('blob'); + const url = URL.createObjectURL(blob); + + const win = window.open(url, '_blank'); + if (!win) { + // Popup blocked (mobile) — fall back to download + const a = document.createElement('a'); + a.href = url; + a.download = `${title.replace(/\s+/g, '_')}_${now.toISOString().slice(0, 10)}.pdf`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } +} diff --git a/src/pwa.js b/src/pwa.js index 2cd206f..dc5a743 100644 --- a/src/pwa.js +++ b/src/pwa.js @@ -276,6 +276,158 @@ export function clearUserCaches() { }); } +/** + * Get the Service Worker we can postMessage to. Resolves with: + * • `navigator.serviceWorker.controller` if it's already in control of the + * page (fastest path), or + * • `registration.active` once `navigator.serviceWorker.ready` resolves + * (covers the first-load case before the SW has claimed the page). + * + * Rejects after `timeoutMs` if no SW becomes available — which would only + * happen in a private/incognito context, an unsupported browser, or when + * registration genuinely failed. + * + * @param {{ timeoutMs?: number }} [opts] + * @returns {Promise} + */ +export async function getActiveServiceWorker({ timeoutMs = 10000 } = {}) { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Workers not supported in this browser'); + } + + // Fastest path — page is already SW-controlled + if (navigator.serviceWorker.controller) { + return navigator.serviceWorker.controller; + } + + // Otherwise wait for the registration to become ready (active SW exists + // for this scope, even if it hasn't claimed THIS page yet) + const ready = navigator.serviceWorker.ready; + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Service-worker readiness timeout')), timeoutMs) + ); + + const registration = await Promise.race([ready, timeout]); + + // The controller may have appeared while we were waiting; otherwise use + // the registration's active worker (we can still postMessage to it — caches + // are shared across the origin) + const sw = navigator.serviceWorker.controller || registration.active; + if (!sw) { + throw new Error('No active service worker available'); + } + return sw; +} + +/** + * Subscribe to controller-change events. The callback fires whenever a new + * Service Worker takes control of the page (e.g. after an SW update or first + * activation on initial load). Useful for re-querying SW-backed state once + * the SW has actually taken over. + * + * @param {() => void} callback + * @returns {() => void} unsubscribe function + */ +export function onServiceWorkerControllerChange(callback) { + if (!('serviceWorker' in navigator)) return () => {}; + const handler = () => { + try { callback(); } catch (e) { console.error('[PWA] controllerchange handler error:', e); } + }; + navigator.serviceWorker.addEventListener('controllerchange', handler); + return () => navigator.serviceWorker.removeEventListener('controllerchange', handler); +} + +/** + * Send a message to the service worker and wait for a single reply with the + * given response type. Waits for the SW to become available if it isn't yet. + * Resolves with the reply payload, or rejects after a timeout. + * + * @template T + * @param {string} requestType - Message type to send (e.g. 'GET_TILE_STATS') + * @param {string} responseType - Message type expected back (e.g. 'TILE_STATS') + * @param {Object} [extra={}] - Extra fields merged into the outgoing message + * @param {number} [timeoutMs=5000] Reply timeout (after the SW is available) + * @param {number} [readyTimeoutMs=10000] Timeout for the SW to be available + * @returns {Promise} + */ +async function requestFromServiceWorker(requestType, responseType, extra = {}, timeoutMs = 5000, readyTimeoutMs = 10000) { + const sw = await getActiveServiceWorker({ timeoutMs: readyTimeoutMs }); + + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + const timer = setTimeout(() => { + channel.port1.close(); + reject(new Error(`Service-worker reply "${responseType}" timed out`)); + }, timeoutMs); + + channel.port1.onmessage = (event) => { + if (event.data?.type === responseType) { + clearTimeout(timer); + channel.port1.close(); + const { type, ...rest } = event.data; + resolve(rest); + } + }; + + sw.postMessage({ type: requestType, ...extra }, [channel.port2]); + }); +} + +/** + * Get statistics about tiles cached locally on this device, broken down by + * provider. Waits up to `readyTimeoutMs` for the service worker to become + * available. Returns null only if the SW genuinely cannot be reached + * (private mode, registration failure, or timeout). + * + * @returns {Promise<{ + * totals: { count: number, estBytes: number }, + * byProvider: Array<{ key: string, label: string, count: number, limit: number, estBytes: number }> + * } | null>} + */ +export async function getTileCacheStats() { + try { + const reply = await requestFromServiceWorker('GET_TILE_STATS', 'TILE_STATS'); + return reply.stats; + } catch (err) { + console.warn('[PWA] getTileCacheStats failed:', err); + return null; + } +} + +/** + * Delete every cached tile from this device. Doesn't touch the app shell, + * modules, or API caches — only the per-provider tile buckets. + * Waits for the SW to be available before sending the request. + * + * @returns {Promise} true if the request was acknowledged + */ +export async function clearTileCaches() { + try { + await requestFromServiceWorker('CLEAR_TILE_CACHES', 'TILE_CACHES_CLEARED'); + return true; + } catch (err) { + console.warn('[PWA] clearTileCaches failed:', err); + return false; + } +} + +/** + * Get total disk used by this origin (Cache API + IndexedDB + OPFS). + * Returns null if the Storage API is not available. + * + * @returns {Promise<{ usage: number, quota: number } | null>} + */ +export async function getStorageEstimate() { + if (!navigator.storage?.estimate) return null; + try { + const { usage, quota } = await navigator.storage.estimate(); + return { usage: usage || 0, quota: quota || 0 }; + } catch (err) { + console.warn('[PWA] getStorageEstimate failed:', err); + return null; + } +} + // ============================================================================ // Auto-initialization // ============================================================================ @@ -313,5 +465,10 @@ export default { applyUpdate, postToServiceWorker, cacheModules, - clearUserCaches + clearUserCaches, + getTileCacheStats, + clearTileCaches, + getStorageEstimate, + getActiveServiceWorker, + onServiceWorkerControllerChange, }; diff --git a/src/remotedb.js b/src/remotedb.js index 0619045..a45fc9b 100644 --- a/src/remotedb.js +++ b/src/remotedb.js @@ -21,13 +21,89 @@ const API_CREDENTIALS = { api_token: '1c46538c712e9b5b' }; +// ============================================================================ +// Server Reachability +// ============================================================================ + +/** Default timeout for API requests (ms) */ +const REQUEST_TIMEOUT = 30_000; + +/** Timeout for the fast reachability probe (ms) */ +const PING_TIMEOUT = 5_000; + +/** Cached result of the last reachability check */ +let _serverReachable = null; + +/** + * Quick probe to determine if the API server is responding. + * Sends a small POST to a lightweight endpoint with a short timeout. + * The result is cached so subsequent calls within the same page load + * return immediately. + * + * @param {boolean} [force=false] - Re-check even if a cached result exists + * @returns {Promise} true if the server responded in time + */ +export async function checkServerReachable(force = false) { + if (_serverReachable !== null && !force) return _serverReachable; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), PING_TIMEOUT); + + try { + const response = await fetch(`${API_BASE}/get_layers.php`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify(API_CREDENTIALS), + signal: controller.signal, + }); + _serverReachable = response.ok; + } catch { + _serverReachable = false; + } finally { + clearTimeout(timer); + } + + console.log('[RemoteDB] Server reachable:', _serverReachable); + return _serverReachable; +} + +/** + * Returns the cached server-reachability flag (synchronous). + * Returns null if checkServerReachable() has not been called yet. + * @returns {boolean|null} + */ +export function isServerReachable() { + return _serverReachable; +} + // ============================================================================ // Core Request Helpers // ============================================================================ +/** + * Create an AbortController that auto-aborts after `ms` milliseconds. + * If the caller already supplied a signal in `options`, it is combined + * so that either the caller's abort or the timeout will cancel the request. + */ +function withTimeout(options, ms = REQUEST_TIMEOUT) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ms); + + // If the caller provided their own signal, chain it + if (options.signal) { + options.signal.addEventListener('abort', () => controller.abort()); + } + + return { + signal: controller.signal, + clear: () => clearTimeout(timer), + }; +} + /** * Perform a GET request to the remote API. * Credentials are sent as URL query parameters. + * Automatically times out after REQUEST_TIMEOUT ms. * * @param {string} endpoint - API endpoint filename (e.g. 'get_district_boundary.php') * @param {Object} [params={}] - Additional query parameters @@ -45,13 +121,15 @@ export async function remoteGet(endpoint, params = {}, options = {}) { console.log('[RemoteDB] GET', url.toString()); + const timeout = withTimeout(options); try { const response = await fetch(url.toString(), { method: 'GET', headers: { 'Accept': 'application/json' }, - ...options + ...options, + signal: timeout.signal, }); if (!response.ok) { @@ -63,14 +141,21 @@ export async function remoteGet(endpoint, params = {}, options = {}) { return data; } catch (error) { + if (error.name === 'AbortError') { + console.error('[RemoteDB] GET timed out:', endpoint); + throw new Error(`Request timed out: ${endpoint}`); + } console.error('[RemoteDB] GET failed:', endpoint, error); throw error; + } finally { + timeout.clear(); } } /** * Perform a POST request to the remote API. * Credentials are included in the JSON body. + * Automatically times out after REQUEST_TIMEOUT ms. * * @param {string} endpoint - API endpoint filename (e.g. 'some_endpoint.php') * @param {Object} [body={}] - Request payload (credentials are merged in) @@ -84,6 +169,7 @@ export async function remotePost(endpoint, body = {}, options = {}) { console.log('[RemoteDB] POST', url); + const timeout = withTimeout(options); try { const response = await fetch(url, { method: 'POST', @@ -92,7 +178,8 @@ export async function remotePost(endpoint, body = {}, options = {}) { 'Accept': 'application/json' }, body: JSON.stringify(payload), - ...options + ...options, + signal: timeout.signal, }); if (!response.ok) { @@ -104,8 +191,14 @@ export async function remotePost(endpoint, body = {}, options = {}) { return data; } catch (error) { + if (error.name === 'AbortError') { + console.error('[RemoteDB] POST timed out:', endpoint); + throw new Error(`Request timed out: ${endpoint}`); + } console.error('[RemoteDB] POST failed:', endpoint, error); throw error; + } finally { + timeout.clear(); } } @@ -170,16 +263,50 @@ export async function getBuildingFootprints() { return remotePost('get_all_footprint_per_district.php'); } +/** + * Fetch the Contours hillshade elevation layer from the server. + * + * Source: table `contours_hillshade` in the local PostgreSQL `public` schema + * (imported from OpenTopography's viz.hh_hillshade). + * + * Expected response: + * { success: true, data: [{ id, elevation, geom: "LINESTRING(...)" | "MULTILINESTRING(...)" | "POLYGON(...)", ... }, ...] } + * + * @returns {Promise} Contours hillshade list + */ +export async function getContoursHillshade() { + return remotePost('get_contours_hillshade.php'); +} + +/** + * Fetch the OSM roads layer from the server. + * + * Source: table `pi_osm_roads` in the local PostgreSQL `public` schema + * (imported from OpenStreetMap road network for the district). + * + * Expected response: + * { success: true, data: [{ id, ..., geom: "LINESTRING(...)" | "MULTILINESTRING(...)", ... }, ...] } + * + * @returns {Promise} OSM roads list + */ +export async function getOSMRoads() { + return remotePost('get_osm_roads.php'); +} + // ============================================================================ // Exports // ============================================================================ export default { + checkServerReachable, + isServerReachable, remoteGet, remotePost, getDistrictBoundary, getLayers, getDistrictParcels, getCollectorZones, - getBuildingFootprints + getBuildingFootprints, + getContoursHillshade, + getOSMRoads, }; diff --git a/src/toast.js b/src/toast.js new file mode 100644 index 0000000..c34405f --- /dev/null +++ b/src/toast.js @@ -0,0 +1,98 @@ +/** + * Lightweight toast notification system. + * + * Usage: + * import { showToast } from '../toast.js'; + * + * showToast('Something went wrong', 'error'); + * showToast('Merge successful!', 'success'); + * showToast('Select two adjacent polygons', 'info'); + */ + +// ── Palette ────────────────────────────────────────────────────────────────── + +const THEMES = { + success: { bg: '#10b981', icon: '\u2705' }, // green + error: { bg: '#ef4444', icon: '\u274c' }, // red + warning: { bg: '#f59e0b', icon: '\u26a0\ufe0f' }, // amber + info: { bg: '#0ea5e9', icon: '\u2139\ufe0f' }, // cyan +}; + +// ── Container (created once, appended to ) ──────────────────────────── + +let container = null; + +function ensureContainer() { + if (container) return container; + container = document.createElement('div'); + container.style.cssText = ` + position: fixed; + top: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + pointer-events: none; + `; + document.body.appendChild(container); + return container; +} + +// ── Public API ────────────────────────────────────────────────────────────── + +/** + * Display a toast notification. + * + * @param {string} message Plain-text message to show. + * @param {'success'|'error'|'warning'|'info'} [type='info'] + * @param {number} [duration=4000] Auto-dismiss time in ms. + */ +export function showToast(message, type = 'info', duration = 4000) { + const parent = ensureContainer(); + const theme = THEMES[type] || THEMES.info; + + const el = document.createElement('div'); + el.style.cssText = ` + background: ${theme.bg}; + color: #fff; + padding: 10px 18px; + border-radius: 8px; + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 13px; + font-weight: 600; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); + pointer-events: auto; + cursor: pointer; + opacity: 0; + transition: opacity 0.25s ease, transform 0.25s ease; + transform: translateY(-8px); + max-width: 420px; + text-align: center; + line-height: 1.4; + `; + el.textContent = `${theme.icon} ${message}`; + + parent.appendChild(el); + + // Animate in + requestAnimationFrame(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + }); + + // Dismiss helper + const dismiss = () => { + el.style.opacity = '0'; + el.style.transform = 'translateY(-8px)'; + setTimeout(() => el.remove(), 300); + }; + + // Click to dismiss early + el.addEventListener('click', dismiss); + + // Auto-dismiss + setTimeout(dismiss, duration); +} diff --git a/src/units.js b/src/units.js new file mode 100644 index 0000000..0e7c5b7 --- /dev/null +++ b/src/units.js @@ -0,0 +1,123 @@ +/** + * Measurement unit formatting — Metric / Imperial. + * + * The active system is persisted in localStorage('measurement-system'). + * Every formatter reads the current setting so the UI updates immediately + * after the user flips the toggle. + * + * All input values are in metres (length) or square metres (area). + */ + +// ── Conversion constants ──────────────────────────────────────────────────── +const M_TO_FT = 3.28084; +const M_TO_MI = 0.000621371; +const SQM_TO_SQFT = 10.7639; +const SQM_TO_ACRE = 0.000247105; +const SQM_TO_SQMI = 3.861e-7; + +// ── System accessor ───────────────────────────────────────────────────────── + +/** @returns {'metric'|'imperial'} */ +export function getSystem() { + return localStorage.getItem('measurement-system') || 'metric'; +} + +// ── Length / distance ─────────────────────────────────────────────────────── + +/** + * Format a length value (in metres) for display. + * Metric: m / km + * Imperial: ft / mi + */ +export function formatLength(metres) { + if (getSystem() === 'imperial') { + const ft = metres * M_TO_FT; + if (ft >= 5280) { + return (Math.round(metres * M_TO_MI * 100) / 100) + ' mi'; + } + return Math.round(ft) + ' ft'; + } + // metric + if (metres > 1000) { + return (Math.round(metres / 1000 * 100) / 100) + ' km'; + } + return (Math.round(metres * 100) / 100) + ' m'; +} + +/** + * Format a length with both large and small units (for info popups). + * Metric: "1.23 km (1,230 m)" or "456 m" + * Imperial: "1.23 mi (6,494 ft)" or "456 ft" + */ +export function formatLengthFull(metres) { + if (getSystem() === 'imperial') { + const ft = metres * M_TO_FT; + const mi = metres * M_TO_MI; + if (ft >= 5280) { + return `${mi.toFixed(2)} mi (${ft.toLocaleString('en', { maximumFractionDigits: 0 })} ft)`; + } + return `${ft.toLocaleString('en', { maximumFractionDigits: 1 })} ft`; + } + if (metres >= 1000) { + return `${(metres / 1000).toFixed(2)} km (${metres.toLocaleString('en', { maximumFractionDigits: 0 })} m)`; + } + return `${metres.toLocaleString('en', { maximumFractionDigits: 1 })} m`; +} + +// ── Area ──────────────────────────────────────────────────────────────────── + +/** + * Format an area value (in square metres) for display. + * Metric: m² / km² + * Imperial: ft² / acres / mi² + */ +export function formatArea(sqMetres) { + if (getSystem() === 'imperial') { + const acres = sqMetres * SQM_TO_ACRE; + if (acres >= 640) { + return (Math.round(sqMetres * SQM_TO_SQMI * 100) / 100) + ' mi²'; + } + if (acres >= 1) { + return (Math.round(acres * 100) / 100) + ' acres'; + } + return Math.round(sqMetres * SQM_TO_SQFT).toLocaleString('en') + ' ft²'; + } + // metric + if (sqMetres > 1000000) { + return (Math.round(sqMetres / 1000000 * 100) / 100) + ' km²'; + } + return (Math.round(sqMetres * 100) / 100) + ' m²'; +} + +/** + * Format an area with both large and small units (for info popups). + * Metric: "1.23 km² (1,230,000 m²)" or "456 m²" + * Imperial: "1.23 mi² (787 acres)" or "2.5 acres" or "456 ft²" + */ +export function formatAreaFull(sqMetres) { + if (getSystem() === 'imperial') { + const sqft = sqMetres * SQM_TO_SQFT; + const acres = sqMetres * SQM_TO_ACRE; + const sqmi = sqMetres * SQM_TO_SQMI; + if (acres >= 640) { + return `${sqmi.toFixed(2)} mi² (${acres.toLocaleString('en', { maximumFractionDigits: 0 })} acres)`; + } + if (acres >= 1) { + return `${acres.toLocaleString('en', { maximumFractionDigits: 1 })} acres (${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²)`; + } + return `${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²`; + } + if (sqMetres > 1000000) { + return `${(sqMetres / 1000000).toFixed(2)} km² (${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²)`; + } + return `${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²`; +} + +// ── Circle helper ─────────────────────────────────────────────────────────── + +/** + * Format the area of a circle given its radius (in metres). + */ +export function formatCircleExtent(radiusMetres) { + return formatArea(Math.PI * radiusMetres * radiusMetres); +} diff --git a/vite.config.js b/vite.config.js index 1ef0b3f..f96db15 100644 --- a/vite.config.js +++ b/vite.config.js @@ -34,12 +34,25 @@ export default defineConfig({ // Target modern browsers that support OPFS target: 'esnext', + // Raise the chunk-size warning threshold. + // Rationale: the two largest chunks are unavoidable — + // • openlayers (~535 kB / ~152 kB gzipped) — the OL library itself, + // used app-wide; further splitting just creates more HTTP round-trips + // • sqlite3.wasm (~856 kB) — a runtime WASM binary, not a JS chunk; + // code-splitting does not apply + // 900 kB silences the noise without hiding genuine regressions. + chunkSizeWarningLimit: 900, + rollupOptions: { output: { manualChunks(id) { 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'; + if (id.includes('node_modules/jspdf')) return 'jspdf'; }, }, },