@rr0/ufoathome 0.111.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{S,O as k,I as R,a as F}from"./rr0-sighting-editor.mjs";class K{constructor(t){if(this.candidates=t,t.length===0)throw new Error("A survey needs at least one candidate");this.current=t[0]}current;get id(){return this.current.id}get name(){return this.current.name}get attribution(){return this.current.attribution}get creditUrl(){return this.current.creditUrl}get contemporary(){return this.current.contemporary}covers(t,e){return this.candidates.some(o=>o.covers(t,e))}async getSurroundings(t,e){let o;for(const n of this.candidates)if(n.covers(t.lat,t.lng))try{const s=await n.getSurroundings(t,e);return this.current=n,s}catch(s){if(o??=s,!(s instanceof S))throw s}throw o??new S("no-coverage","No survey reaches this place")}}class w{constructor(t,e,o,n){this.heights=t,this.building=e,this.width=o,this.height=n}static ROOF_PERCENTILE=.9;static STEP_M=.9;static RECTANGLE_FILL=.72;static MIN_DEPTH_M=2.5;static MAX_PARTS=24;static MAX_CELLS=640;buildings(t){const e=t.metresPerPixel,o=t.minAreaM2??12,n=new Uint8Array(this.width*this.height),s=[];for(let r=0;r<this.building.length;r++){if(!this.building[r]||n[r])continue;const a=this.patchFrom(r,n);if(!(a.length*e*e<o))for(const i of this.buildingsOf(a,e,o))Math.hypot(i.eastM,i.northM)<=t.radiusM&&s.push(i)}return s.sort((r,a)=>Math.hypot(r.eastM,r.northM)-Math.hypot(a.eastM,a.northM)),s.slice(0,t.maxBuildings??60)}patchFrom(t,e){const o=[],n=[t];for(e[t]=1;n.length>0;){const s=n.pop();o.push(s);const r=s%this.width,a=Math.floor(s/this.width);for(const[i,h]of[[1,0],[-1,0],[0,1],[0,-1]]){const u=r+i,c=a+h;if(u<0||c<0||u>=this.width||c>=this.height)continue;const l=c*this.width+u;this.building[l]&&!e[l]&&Math.abs(this.heights[l]-this.heights[s])<=w.STEP_M&&(e[l]=1,n.push(l))}}return o}buildingsOf(t,e,o){const n=this.width/2,s=this.height/2,r=[],a=[],i=[];for(const h of t){const u=h%this.width,c=Math.floor(h/this.width);r.push((u+.5-n)*e),a.push((s-c-.5)*e),i.push(this.heights[h])}return w.ofPoints(r,a,i,e,o)}static ofPoints(t,e,o,n,s,r=w.RECTANGLE_FILL,a=[]){const i=[];for(let m=0;m<t.length;m++)for(const[E,P]of[[-.5,-.5],[.5,-.5],[-.5,.5],[.5,.5]])i.push([t[m]+E*n,e[m]+P*n]);const h=w.hull(i),{angle:u,rectangle:c}=w.smallestRectangle(h),l=t.length*n*n,M=(c.maxU-c.minU)*(c.maxV-c.minV);if(l/M>=r||l<4*s)return[w.building(c,u,w.roof(o))];const f=h.map((m,E)=>({from:{east:m[0],north:m[1]},to:{east:h[(E+1)%h.length][0],north:h[(E+1)%h.length][1]}})),d=w.dominantAngles([...a,...f]);return w.parts(t,e,o,d.length>0?d:[u],n,s)}static dominantAngles(t,e=3){const o=new Map;for(const n of t){const s=Math.hypot(n.to.east-n.from.east,n.to.north-n.from.north);if(s<1e-6)continue;const r=(Math.atan2(n.to.north-n.from.north,n.to.east-n.from.east)%(Math.PI/2)+Math.PI/2)%(Math.PI/2),a=Math.round(r/(Math.PI/60));o.set(a,(o.get(a)??0)+s)}return[...o.entries()].sort((n,s)=>s[1]-n[1]).slice(0,e).map(([n])=>n*Math.PI/60)}static parts(t,e,o,n,s,r){let a=t.map((h,u)=>u);const i=[];for(let h=0;h<w.MAX_PARTS&&a.length>0;h++){let u;for(const M of n){const f=w.largestIn(t,e,a,M,s);f&&(u===void 0||f.area>u.area)&&(u=f)}if(u===void 0||u.area<r)break;const c=u.covered;Math.min(u.rectangle.maxU-u.rectangle.minU,u.rectangle.maxV-u.rectangle.minV)>=w.MIN_DEPTH_M&&i.push(w.building(u.rectangle,u.angle,w.roof([...c].map(M=>o[M])))),a=a.filter(M=>!c.has(M))}return i}static largestIn(t,e,o,n,s){const r=Math.cos(n),a=Math.sin(n);let i=1/0,h=-1/0,u=1/0,c=-1/0;const l=[],M=[];for(const D of o){const y=t[D]*r+e[D]*a,b=-t[D]*a+e[D]*r;l.push(y),M.push(b),i=Math.min(i,y),h=Math.max(h,y),u=Math.min(u,b),c=Math.max(c,b)}const f=Math.max(s*Math.SQRT2,Math.max(h-i,c-u)/w.MAX_CELLS),d=Math.max(1,Math.ceil((h-i)/f+1e-9)),m=Math.max(1,Math.ceil((c-u)/f+1e-9)),E=new Uint8Array(d*m),P=o.map((D,y)=>{const b=Math.min(d-1,Math.max(0,Math.floor((l[y]-i)/f))),T=Math.min(m-1,Math.max(0,Math.floor((M[y]-u)/f)));return E[T*d+b]=1,T*d+b}),p=w.largestRectangle(E,d,m);if(p===void 0)return;const L=new Set;return o.forEach((D,y)=>{const b=P[y]%d,T=Math.floor(P[y]/d);b>=p.column&&b<p.column+p.columns&&T>=p.row&&T<p.row+p.rows&&L.add(D)}),{area:p.area*f*f,angle:n,covered:L,rectangle:{minU:i+p.column*f,maxU:i+(p.column+p.columns)*f,minV:u+p.row*f,maxV:u+(p.row+p.rows)*f}}}static largestRectangle(t,e,o){const n=new Array(e).fill(0);let s;for(let r=0;r<o;r++){for(let i=0;i<e;i++)n[i]=t[r*e+i]?n[i]+1:0;const a=[];for(let i=0;i<=e;i++){const h=i===e?0:n[i];for(;a.length>0&&n[a[a.length-1]]>=h;){const u=a.pop(),c=a.length===0?i:i-a[a.length-1]-1,l=n[u]*c;(s===void 0||l>s.area)&&(s={column:i-c,row:r-n[u]+1,columns:c,rows:n[u],area:l})}a.push(i)}}return s}static roof(t){const e=[...t].sort((o,n)=>o-n);return Math.max(2,e[Math.min(e.length-1,Math.floor(e.length*w.ROOF_PERCENTILE))])}static building(t,e,o){const n=Math.cos(e),s=Math.sin(e),r=t.maxU-t.minU,a=t.maxV-t.minV,i=(t.minU+t.maxU)/2,h=(t.minV+t.maxV)/2,c=((90-(r<a?e:e+Math.PI/2)*180/Math.PI)%180+180)%180;return{eastM:w.tenth(i*n-h*s),northM:w.tenth(i*s+h*n),widthM:w.tenth(Math.max(r,a)),lengthM:w.tenth(Math.min(r,a)),heightM:w.tenth(o),headingDeg:Math.round(c*10)/10}}static smallestRectangle(t){let e={area:1/0,angle:0,rectangle:{minU:0,maxU:0,minV:0,maxV:0}};for(let o=0;o<Math.max(1,t.length);o++){const[n,s]=t[o]??[0,0],[r,a]=t[(o+1)%t.length]??[1,0],i=Math.atan2(a-s,r-n),h=Math.cos(i),u=Math.sin(i);let c=1/0,l=-1/0,M=1/0,f=-1/0;for(const[m,E]of t){const P=m*h+E*u,p=-m*u+E*h;c=Math.min(c,P),l=Math.max(l,P),M=Math.min(M,p),f=Math.max(f,p)}const d=(l-c)*(f-M);d<e.area&&(e={area:d,angle:i,rectangle:{minU:c,maxU:l,minV:M,maxV:f}})}return e}static hull(t){const e=[...t].sort((a,i)=>a[0]-i[0]||a[1]-i[1]),o=e.filter((a,i)=>i===0||a[0]!==e[i-1][0]||a[1]!==e[i-1][1]);if(o.length<3)return o;const n=(a,i,h)=>(i[0]-a[0])*(h[1]-a[1])-(i[1]-a[1])*(h[0]-a[0]),s=[];for(const a of o){for(;s.length>=2&&n(s[s.length-2],s[s.length-1],a)<=0;)s.pop();s.push(a)}const r=[];for(const a of[...o].reverse()){for(;r.length>=2&&n(r[r.length-2],r[r.length-1],a)<=0;)r.pop();r.push(a)}return[...s.slice(0,-1),...r.slice(0,-1)]}static tenth(t){return Math.round(t*10)/10}}class x{constructor(t,e,o,n){this.heights=t,this.vegetation=e,this.width=o,this.height=n}static SUMMIT_RADIUS_M=2.5;static CROWN_REACH_M=8;static MAX_WIDTH_M=16;static MIN_WIDTH_M=2;static tenth(t){return Math.round(t*10)/10}trees(t){const e=t.metresPerPixel,o=t.minHeightM??4,n=t.eyeM??1.6,s=t.minElevationDeg??2,r=Math.max(1,Math.round(x.SUMMIT_RADIUS_M/e)),a=Math.round(x.CROWN_REACH_M/e),i=this.width/2,h=this.height/2,u=[];for(let c=0;c<this.height;c++)for(let l=0;l<this.width;l++){const M=this.canopy(l,c);if(M<o)continue;const f=(l+.5-i)*e,d=(h-c-.5)*e,m=Math.hypot(f,d);if(m>t.radiusM||!this.isSummit(l,c,M,r))continue;const E=Math.atan2(M-n,Math.max(m,1))*180/Math.PI;E<s||u.push({tree:{eastM:x.tenth(f),northM:x.tenth(d),heightM:x.tenth(M),widthM:x.tenth(this.crownWidth(l,c,M,e,a))},elevation:E})}return u.sort((c,l)=>l.elevation-c.elevation),u.slice(0,t.maxTrees??150).map(c=>c.tree)}canopy(t,e){if(t<0||e<0||t>=this.width||e>=this.height)return 0;const o=e*this.width+t;return this.vegetation[o]?this.heights[o]:0}isSummit(t,e,o,n){for(let s=-n;s<=n;s++)for(let r=-n;r<=n;r++){if(r===0&&s===0)continue;const a=this.canopy(t+r,e+s);if(a>o||a===o&&(s<0||s===0&&r<0))return!1}return!0}crownWidth(t,e,o,n,s){let r=0;for(let i=-s;i<=s;i++)for(let h=-s;h<=s;h++)Math.hypot(h,i)*n<=x.CROWN_REACH_M&&this.canopy(t+h,e+i)>=o*.5&&r++;const a=2*Math.sqrt(r*n*n/Math.PI);return Math.max(x.MIN_WIDTH_M,Math.min(a,x.MAX_WIDTH_M))}}class A{static WALLS={1:"stone",2:"meuliere",3:"concrete",4:"brick",5:"block",6:"wood",9:"other"};static ROOFS={1:"tile",2:"slate",3:"zinc",4:"concrete",9:"other"};static WALL_COLOURS={stone:"#b9ae9b",meuliere:"#b8a78a",concrete:"#b4b2ac",brick:"#a65f48",block:"#d0c6b2",wood:"#7d5c3e",other:"#c9c0ae"};static PLASTER="#d3cab8";static ROOF_COLOURS={tile:"#a0583f",slate:"#4a5059",zinc:"#8d969c",concrete:"#8f8e8a",other:"#7a7872"};static VARIATION=.12;static of(t,e){const o=String(t??"").trim();if(!/^\d{1,2}$/.test(o))return;const n=Number(o),s=n%10!==0?n%10:Math.floor(n/10);return(e==="wall"?A.WALLS:A.ROOFS)[String(s)]}static roofColor(t){return t?A.ROOF_COLOURS[t]:void 0}static fit(t){return t.map(e=>{if(e.facade)return e;const o={color:A.wallColor(e)};return{...e,facade:o}})}static wallColor(t){const e=t.wallMaterial?A.WALL_COLOURS[t.wallMaterial]:A.PLASTER,o=Math.sin(t.eastM*12.9898+t.northM*78.233)*43758.5453,n=1+(o-Math.floor(o)-.5)*2*A.VARIATION,s=r=>Math.max(0,Math.min(255,Math.round(parseInt(e.slice(r,r+2),16)*n))).toString(16).padStart(2,"0");return`#${s(1)}${s(3)}${s(5)}`}}class g{static MIN_RISE_M=1.2;static MIN_EAVES_M=2.5;static MIN_SEEN=.35;static NO_DATA=-9999;static BANDS=7;static INSET_M=.9;static LIFT=1.18;static PEAK=.3;static LEVEL=.45;static fit(t,e,o){return!e&&!o&&!t.some(n=>n.roofMaterial)?t:t.map(n=>{const s=e?g.shapeOf(n,e):void 0,r=(o?g.colorOf(n,o):void 0)??A.roofColor(n.roofMaterial);if(!s&&!r)return n;const a={shape:s?.shape??"flat",...s?.ridge==="depth"?{ridge:"depth"}:{},riseM:s?.riseM??0,...r?{color:r}:{}},i=s&&s.shape!=="flat"?s.topM:n.heightM;return{...n,heightM:i,roof:a}})}static world(t,e,o){const n=t.headingDeg*Math.PI/180;return{east:t.eastM+e*Math.cos(n)+o*Math.sin(n),north:t.northM-e*Math.sin(n)+o*Math.cos(n)}}static shapeOf(t,e){const o=g.BANDS,n=Math.max(e.metresPerPixel,.5),s=t.widthM/2-g.INSET_M,r=t.lengthM/2-g.INSET_M;if(s<=0||r<=0)return;const a=[],i=Array.from({length:o},()=>({sum:0,n:0})),h=Array.from({length:o},()=>({sum:0,n:0}));let u=0;for(let y=-s;y<=s;y+=n)for(let b=-r;b<=r;b+=n){u++;const{east:T,north:X}=g.world(t,y,b),C=Math.floor(T/e.metresPerPixel+e.size/2),z=Math.floor(e.size/2-X/e.metresPerPixel);if(C<0||z<0||C>=e.size||z>=e.size)continue;const V=z*e.size+C,U=e.heights[V];if(e.vegetation[V]||U===g.NO_DATA||!(U>0))continue;a.push(U);const W=i[Math.min(o-1,Math.floor((y+s)/(2*s)*o))],$=h[Math.min(o-1,Math.floor((b+r)/(2*r)*o))];W.sum+=U,W.n++,$.sum+=U,$.n++}if(u===0||a.length/u<g.MIN_SEEN)return;a.sort((y,b)=>y-b);const c=g.percentile(a,.08),l=g.percentile(a,.92),M=l-c;if(c<g.MIN_EAVES_M||M>Math.min(t.widthM,t.lengthM))return;if(M<g.MIN_RISE_M)return{shape:"flat",riseM:0,topM:l};const f=g.profile(i),d=g.profile(h);if(!f||!d)return;const m=g.peak(d)/M,E=g.peak(f)/M,P=g.spread(f)/M,p=g.spread(d)/M,L=m>=g.PEAK&&P<=g.LEVEL,D=E>=g.PEAK&&p<=g.LEVEL;if(L&&(!D||m>=E))return{shape:"gable",ridge:"width",riseM:M,topM:l};if(D)return{shape:"gable",ridge:"depth",riseM:M,topM:l};if(m>=g.PEAK&&E>=g.PEAK/2)return{shape:"hip",ridge:"width",riseM:M,topM:l}}static profile(t){const e=t.map(o=>o.n>0?o.sum/o.n:NaN);return e.filter(Number.isFinite).length>=t.length-1?e.map((o,n)=>Number.isFinite(o)?o:g.neighbour(e,n)):void 0}static neighbour(t,e){return Number.isFinite(t[e-1])?t[e-1]:t[e+1]}static peak(t){const e=Math.max(1,Math.floor(t.length/3)),o=s=>s.reduce((r,a)=>r+a,0)/s.length,n=[...t.slice(0,e),...t.slice(t.length-e)];return o(t.slice(e,t.length-e))-o(n)}static spread(t){return Math.max(...t)-Math.min(...t)}static percentile(t,e){return t[Math.min(t.length-1,Math.floor(t.length*e))]}static colorOf(t,e){const o=Math.max(e.metresPerPixel,.5),n=Math.max(t.widthM/2-g.INSET_M,o),s=Math.max(t.lengthM/2-g.INSET_M,o),r=[],a=[],i=[];for(let c=-n;c<=n;c+=o)for(let l=-s;l<=s;l+=o){const{east:M,north:f}=g.world(t,c,l),d=Math.floor(M/e.metresPerPixel+e.size/2),m=Math.floor(e.size/2-f/e.metresPerPixel);if(d<0||m<0||d>=e.size||m>=e.size)continue;const E=(m*e.size+d)*e.samples,P=e.values[E],p=e.values[E+1],L=e.values[E+2];p>P&&p>L||(r.push(P),a.push(p),i.push(L))}if(r.length===0)return;const h=c=>c.sort((l,M)=>l-M)[Math.floor(c.length/2)],u=c=>Math.min(255,Math.round(c*g.LIFT)).toString(16).padStart(2,"0");return`#${u(h(r))}${u(h(a))}${u(h(i))}`}}class v{static DEFAULT_HEIGHT_M=6;static METRES_PER_DEGREE=111320;static STREET_SPACING_M=35;static KERB_OFFSET_M=1;static DEFAULT_WIDTH_M=5.5;static PATIENCE_MS=15e3;static MAX=150;client;constructor(t={}){this.client=new k(t)}static today(){return new Date().toISOString().slice(0,10)}async near(t,e){const o=e/v.METRES_PER_DEGREE,n=e/(v.METRES_PER_DEGREE*Math.cos(t.lat*Math.PI/180)),s=[t.lat-o,t.lng-n,t.lat+o,t.lng+n].map(i=>i.toFixed(6)).join(",");let r;const a=new Promise(i=>{r=setTimeout(()=>i(void 0),v.PATIENCE_MS)});try{const i=await Promise.race([this.client.query(`[out:json][timeout:25];(node["highway"="street_lamp"](${s});way["highway"]["lit"="yes"](${s}););out geom;`),a]);return i?v.lampsFrom(i,t,e):[]}catch{return[]}finally{clearTimeout(r)}}static lampsFrom(t,e,o){const n=[];for(const r of t.elements??[]){if(r.type==="way"){v.alongStreet(r,e,n);continue}if(typeof r.lat!="number"||typeof r.lon!="number")continue;const a=(r.lon-e.lng)*v.METRES_PER_DEGREE*Math.cos(e.lat*Math.PI/180),i=(r.lat-e.lat)*v.METRES_PER_DEGREE;if(Math.hypot(a,i)>o)continue;const h=Number.parseFloat(r.tags?.height??"");n.push({eastM:Math.round(a*10)/10,northM:Math.round(i*10)/10,...Number.isFinite(h)&&h>=2&&h<=20?{heightM:h}:{}})}const s=n.filter(r=>Math.hypot(r.eastM,r.northM)<=o);return n.length=0,n.push(...s),n.sort((r,a)=>Math.hypot(r.eastM,r.northM)-Math.hypot(a.eastM,a.northM)),n.slice(0,v.MAX)}static alongStreet(t,e,o){const n=t.geometry;if(!n||n.length<2)return;const s=f=>[(f.lon-e.lng)*v.METRES_PER_DEGREE*Math.cos(e.lat*Math.PI/180),(f.lat-e.lat)*v.METRES_PER_DEGREE],r=n.map(s),a=Number.parseFloat(t.tags?.width??""),i=Number.parseInt(t.tags?.lanes??"",10),u=(Number.isFinite(a)&&a>0?a:Number.isFinite(i)&&i>0?i*3.5:v.DEFAULT_WIDTH_M)/2+v.KERB_OFFSET_M;let c=1,l=v.STREET_SPACING_M/2,M=0;for(let f=1;f<r.length;f++){const[d,m]=r[f-1],[E,P]=r[f],p=Math.hypot(E-d,P-m);if(p===0)continue;const L=(E-d)/p,D=(P-m)/p;for(;l<=M+p;l+=v.STREET_SPACING_M){const y=l-M;o.push({eastM:Math.round((d+L*y+D*u*c)*10)/10,northM:Math.round((m+D*y-L*u*c)*10)/10}),c=-c}M+=p}}}class j{id="ign-lidar-hd";name="IGN LiDAR HD";attribution="Canopy and roofs © IGN, LiDAR HD (Licence Ouverte 2.0), as surveyed on a day the service does not give";creditUrl="https://geoservices.ign.fr/lidarhd";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new R(t),this.lamps=e}covers(t,e){return R.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new S("no-coverage","The LiDAR HD survey covers France only");const o=R.lidarRadius(e),[n,s,r,a,i]=await Promise.all([this.platform.lidar(t,o),this.platform.photo(t,o).catch(()=>{}),this.lamps?.near(t,e),this.platform.laserDate(t),this.platform.photoDate(t)]);return{trees:new x(n.heights,n.vegetation,n.size,n.size).trees({metresPerPixel:n.metresPerPixel,radiusM:o}),buildings:A.fit(g.fit(new w(n.heights,n.building,n.size,n.size).buildings({metresPerPixel:n.metresPerPixel,radiusM:o,maxBuildings:300}),n,s)),bridges:[],lines:[],...r?{lamps:r}:{},dates:{...a?{laser:a}:{},...s&&i?{photo:i}:{},...r?.length?{lamps:v.today()}:{}}}}}class I{static METRES_PER_DEGREE=111320;static DEFAULT_HEIGHT_M=6;static FLOOR_HEIGHT_M=3;static FINE_M=.5;static COARSE_M=1;static MAX_CELLS=4e4;static RECTANGLE_FILL=.9;static buildings(t,e,o,n=12,s=300){const r=[];for(const a of t){const i=I.rings(a,e);if(i.length===0)continue;const h=Number(a.properties.nombre_d_etages),u=Number(a.properties.hauteur),c=Number.isFinite(u)&&u>0?u:Number.isFinite(h)&&h>0?h*I.FLOOR_HEIGHT_M:I.DEFAULT_HEIGHT_M;for(const l of I.rectanglesOf(i,c,n)){if(Math.hypot(l.eastM,l.northM)>o)continue;const M=A.of(a.properties.materiaux_des_murs,"wall"),f=A.of(a.properties.materiaux_de_la_toiture,"roof");r.push({...l,...Number.isFinite(h)&&h>0?{floors:h}:{},...M?{wallMaterial:M}:{},...f?{roofMaterial:f}:{}})}}return r.sort((a,i)=>Math.hypot(a.eastM,a.northM)-Math.hypot(i.eastM,i.northM)),r.slice(0,s)}static named(t,e,o){const n=e.map(s=>({name:typeof s.properties.toponyme=="string"?s.properties.toponyme:"",rings:I.rings(s,o)})).filter(s=>s.name.length>0&&s.rings.length>0);return n.length===0?t:t.map(s=>{const r=n.find(a=>I.inside(s.eastM,s.northM,a.rings));return r?{...s,name:r.name}:s})}static inside(t,e,o){let n=0;for(const s of o)for(let r=0;r+1<s.length;r++){const a=s[r],i=s[r+1];a.north<=e!=i.north<=e&&a.east+(e-a.north)/(i.north-a.north)*(i.east-a.east)>t&&n++}return n%2===1}static rings(t,e){const o=t.geometry,n=o.type==="Polygon"?[o.coordinates]:o.type==="MultiPolygon"?o.coordinates:[],s=Math.cos(e.lat*Math.PI/180),r=[];for(const a of n)for(const i of a)r.push(i.map(([h,u])=>({east:(h-e.lng)*I.METRES_PER_DEGREE*s,north:(u-e.lat)*I.METRES_PER_DEGREE})));return r}static rectanglesOf(t,e,o){let n=1/0,s=-1/0,r=1/0,a=-1/0;for(const l of t)for(const M of l)n=Math.min(n,M.east),s=Math.max(s,M.east),r=Math.min(r,M.north),a=Math.max(a,M.north);let i=I.FINE_M;(s-n)*(a-r)/(i*i)>I.MAX_CELLS&&(i=I.COARSE_M);const h=[],u=[];for(let l=r+i/2;l<a;l+=i){const M=[];for(const f of t)for(let d=0;d+1<f.length;d++){const m=f[d],E=f[d+1];m.north<=l!=E.north<=l&&M.push(m.east+(l-m.north)/(E.north-m.north)*(E.east-m.east))}M.sort((f,d)=>f-d);for(let f=0;f+1<M.length;f+=2)for(let d=Math.ceil((M[f]-i/2)/i)*i+i/2;d<M[f+1];d+=i)h.push(d),u.push(l)}if(h.length*i*i<o)return[];const c=t.flatMap(l=>l.slice(0,-1).map((M,f)=>({from:M,to:l[f+1]})));return w.ofPoints(h,u,new Array(h.length).fill(e),i,o,I.RECTANGLE_FILL,c)}}class q{id="ign-lidar-topo";name="IGN LiDAR HD + BD TOPO";attribution="Canopy © IGN, LiDAR HD; buildings, bridges and lines © IGN, BD TOPO (Licence Ouverte 2.0), as surveyed on days the services do not give";creditUrl="https://geoservices.ign.fr/lidarhd";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new R(t),this.lamps=e}covers(t,e){return R.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new S("no-coverage","The IGN's surveys cover France only");const o=R.radius(e),[n,s,r,a,i,h]=await Promise.allSettled([this.platform.lidar(t,o),this.platform.vectors(t,o,!0),this.platform.photo(t,o),this.lamps?.near(t,e)??Promise.resolve(void 0),this.platform.laserDate(t),this.platform.photoDate(t)]),u=R.lidarRadius(o);if(n.status==="rejected"&&s.status==="rejected")throw n.reason;const c=n.status==="fulfilled"?n.value:void 0,l=s.status==="fulfilled"?s.value:void 0,M=l!==void 0&&l.buildings.length===0&&l.bridges.length===0&&l.lines.length===0;if(n.status==="rejected"&&n.reason instanceof S&&n.reason.reason==="no-coverage"&&M)throw n.reason;return{trees:c?new x(c.heights,c.vegetation,c.size,c.size).trees({metresPerPixel:c.metresPerPixel,radiusM:u}):[],buildings:this.roofed(this.buildings(c,l,t,o),c,r.status==="fulfilled"?r.value:void 0),bridges:l?F.bridges(l,t,o,c?.structures):[],lines:l?F.lines(l,t,o,c?.structures):[],...a.status==="fulfilled"&&a.value?{lamps:a.value}:{},dates:{...n.status==="fulfilled"&&i.status==="fulfilled"&&i.value?{laser:i.value}:{},...r.status==="fulfilled"&&h.status==="fulfilled"&&h.value?{photo:h.value}:{},...l?.mapDate?{map:l.mapDate}:{},...a.status==="fulfilled"&&a.value?.length?{lamps:v.today()}:{}}}}roofed(t,e,o){return A.fit(g.fit(t,e,o))}buildings(t,e,o,n){const s=e?I.buildings(e.buildings,o,n):[],r=s.length>0||t===void 0?s:new w(t.heights,t.building,t.size,t.size).buildings({metresPerPixel:t.metresPerPixel,radiusM:R.lidarRadius(n),maxBuildings:300});return e?I.named(r,e.zones,o):r}}class Y{id="ign-bd-topo";name="IGN BD TOPO";attribution="Buildings, bridges and lines © IGN, BD TOPO (Licence Ouverte 2.0), as drawn on a day the service does not give";creditUrl="https://geoservices.ign.fr/bdtopo";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new R(t),this.lamps=e}covers(t,e){return R.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new S("no-coverage","BD TOPO covers France only");const o=R.radius(e),[n,s,r,a]=await Promise.all([this.platform.vectors(t,o,!0),this.platform.photo(t,o).catch(()=>{}),this.lamps?.near(t,e),this.platform.photoDate(t)]);return{trees:[],buildings:A.fit(g.fit(I.named(I.buildings(n.buildings,t,o),n.zones,t),void 0,s)),bridges:F.bridges(n,t,o,void 0),lines:F.lines(n,t,o,void 0),...r?{lamps:r}:{},dates:{...n.mapDate?{map:n.mapDate}:{},...s&&a?{photo:a}:{},...r?.length?{lamps:v.today()}:{}}}}}class N{static ROOF_SHAPES={flat:"flat",gabled:"gable",gambrel:"gable",saltbox:"gable","half-hipped":"hip",hipped:"hip",pyramidal:"hip",mansard:"hip",round:"gable"};static WALL_MATERIALS={brick:"brick",stone:"stone",granite:"stone",sandstone:"stone",limestone:"stone",concrete:"concrete",cement_block:"block",block:"block",wood:"wood",timber_framing:"wood"};static ROOF_MATERIALS={roof_tiles:"tile",tiles:"tile",tile:"tile",slate:"slate",metal:"zinc",zinc:"zinc",tin:"zinc",copper:"zinc",concrete:"concrete",eternit:"other"};static COLOURS={white:"#e8e6e0",grey:"#8c8c8a",gray:"#8c8c8a",black:"#3a3a3a",red:"#a8483a",brown:"#6e4f38",beige:"#d6c9a8",yellow:"#d8c070",orange:"#c9773a",green:"#5f7a52",blue:"#506a8a",tan:"#c0a37a",cream:"#e6dcc0",pink:"#c99a96",silver:"#b4b6b6"};static height(t){const e=Number.parseFloat((t??"").replace(",","."));return Number.isFinite(e)&&e>=2&&e<=500?e:void 0}static levels(t){const e=Number.parseInt(t??"",10);return Number.isFinite(e)&&e>=1&&e<=150?e:void 0}static roofShape(t){return N.ROOF_SHAPES[(t??"").trim().toLowerCase()]}static wallMaterial(t){return N.WALL_MATERIALS[(t??"").trim().toLowerCase()]}static roofMaterial(t){return N.ROOF_MATERIALS[(t??"").trim().toLowerCase()]}static colour(t){const e=(t??"").trim().toLowerCase();return/^#[0-9a-f]{6}$/.test(e)?e:/^#[0-9a-f]{3}$/.test(e)?`#${e[1]}${e[1]}${e[2]}${e[2]}${e[3]}${e[3]}`:N.COLOURS[e]}}class O{id="osm-buildings";name="OpenStreetMap buildings";attribution="Buildings © OpenStreetMap contributors (ODbL), as drawn on the day of the data, which a recording is not";creditUrl="https://www.openstreetmap.org/copyright";contemporary=!0;static MAX_RADIUS_M=1500;static MIN_AREA_M2=12;static MAX_BUILDINGS=300;client;lamps;constructor(t={},e){this.client=new k(t),this.lamps=e}covers(){return!0}async getSurroundings(t,e){const o=Math.min(e,O.MAX_RADIUS_M);let n;try{n=await this.client.query(`[out:json][timeout:30];(way["building"](around:${o},${t.lat},${t.lng});relation["building"]["type"="multipolygon"](around:${o},${t.lat},${t.lng}););out geom;`)}catch(a){throw new S("unreachable",a instanceof Error?a.message:String(a))}const s=await this.lamps?.near(t,e),r=(n.osm3s?.timestamp_osm_base??"").slice(0,10);return{trees:[],buildings:A.fit(O.buildings(n,t,o)),bridges:[],lines:[],...s?{lamps:s}:{},dates:{.../^\d{4}-\d{2}-\d{2}$/.test(r)?{map:r}:{},...s?.length?{lamps:v.today()}:{}}}}static buildings(t,e,o){const n=[];for(const s of t.elements??[]){const r=O.ringsOf(s);if(r.length===0)continue;const a=s.tags??{},i=N.levels(a["building:levels"]),h=N.height(a.height),u={geometry:{type:"Polygon",coordinates:r},properties:{...h?{hauteur:h}:{},...i?{nombre_d_etages:i}:{}}};for(const c of I.buildings([u],e,o,O.MIN_AREA_M2,50))n.push(O.described(c,a,h!==void 0))}return n.sort((s,r)=>Math.hypot(s.eastM,s.northM)-Math.hypot(r.eastM,r.northM)),n.slice(0,O.MAX_BUILDINGS)}static ringsOf(t){const e=n=>{if(!n||n.length<4)return;const s=n.map(i=>[i.lon,i.lat]),r=s[0],a=s[s.length-1];return r[0]===a[0]&&r[1]===a[1]?s:void 0};if(t.type==="way"){const n=e(t.geometry);return n?[n]:[]}return(t.members??[]).filter(n=>n.role==="outer").map(n=>e(n.geometry)).filter(n=>n!==void 0).slice(0,1)}static described(t,e,o){const n=N.wallMaterial(e["building:material"]),s=N.roofMaterial(e["roof:material"]),r=N.colour(e["building:colour"]),a=N.colour(e["roof:colour"])??A.roofColor(s),i=N.roofShape(e["roof:shape"]),h=r?{color:r}:void 0;let u,c=t.heightM;if(i==="gable"||i==="hip"){const l=N.height(e["roof:height"]),M=Math.round((l??Math.min(5,Math.max(1.5,.29*Math.min(t.widthM,t.lengthM))))*10)/10;o||(c=Math.round((t.heightM+M)*10)/10),u={shape:i,riseM:M,...e["roof:orientation"]==="across"?{ridge:"depth"}:{},...a?{color:a}:{}}}else(i==="flat"||a)&&(u={shape:"flat",riseM:0,...a?{color:a}:{}});return{...t,heightM:c,...e.name?{name:e.name}:{},...n?{wallMaterial:n}:{},...s?{roofMaterial:s}:{},...h?{facade:h}:{},...u?{roof:u}:{}}}}const _=[{id:"ign-lidar-topo",name:"IGN LiDAR HD + BD TOPO",credit:"© IGN, LiDAR HD and BD TOPO, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/lidarhd",create:()=>new q({},new v)},{id:"ign-lidar-hd",name:"IGN LiDAR HD",credit:"© IGN, LiDAR HD, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/lidarhd",create:()=>new j({},new v)},{id:"ign-bd-topo",name:"IGN BD TOPO",credit:"© IGN, BD TOPO, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/bdtopo",create:()=>new Y({},new v)},{id:"osm-buildings",name:"OpenStreetMap buildings",credit:"Buildings and street lamps © OpenStreetMap contributors (ODbL)",creditUrl:"https://www.openstreetmap.org/copyright",create:()=>new O({},new v)}];class G{static DEFAULT_ID=_[0].id;static WORLD_ID="osm-buildings";static NONE="none";static byId(t){return t===G.NONE?void 0:_.find(e=>e.id===t)??_[0]}static providerFor(t,e,o){const n=G.byId(t)?.create();if(t!==void 0&&t!==G.DEFAULT_ID)return n?.covers(e,o)?n:void 0;const s=_.find(a=>a.id===G.WORLD_ID).create(),r=[n,s].filter(a=>a!==void 0&&a.covers(e,o));return r.length>1?new K(r):r[0]}}export{_ as SURROUNDINGS_SOURCES,G as SurroundingsSources};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rr0/ufoathome",
3
3
  "type": "module",
4
- "version": "0.111.0",
4
+ "version": "0.113.0",
5
5
  "description": "UFO@home — record and replay a UFO sighting's shape, movement and appearance",
6
6
  "author": "Jérôme Beau <rr0@rr0.org> (https://rr0.org)",
7
7
  "license": "MIT",
@@ -1 +0,0 @@
1
- import{O as X,I as T,S as F,a as L}from"./rr0-sighting-editor.mjs";class v{constructor(t,e,n,s){this.heights=t,this.building=e,this.width=n,this.height=s}static ROOF_PERCENTILE=.9;static STEP_M=.9;static RECTANGLE_FILL=.72;static MIN_DEPTH_M=2.5;static MAX_PARTS=24;static MAX_CELLS=640;buildings(t){const e=t.metresPerPixel,n=t.minAreaM2??12,s=new Uint8Array(this.width*this.height),a=[];for(let o=0;o<this.building.length;o++){if(!this.building[o]||s[o])continue;const r=this.patchFrom(o,s);if(!(r.length*e*e<n))for(const i of this.buildingsOf(r,e,n))Math.hypot(i.eastM,i.northM)<=t.radiusM&&a.push(i)}return a.sort((o,r)=>Math.hypot(o.eastM,o.northM)-Math.hypot(r.eastM,r.northM)),a.slice(0,t.maxBuildings??60)}patchFrom(t,e){const n=[],s=[t];for(e[t]=1;s.length>0;){const a=s.pop();n.push(a);const o=a%this.width,r=Math.floor(a/this.width);for(const[i,h]of[[1,0],[-1,0],[0,1],[0,-1]]){const M=o+i,c=r+h;if(M<0||c<0||M>=this.width||c>=this.height)continue;const l=c*this.width+M;this.building[l]&&!e[l]&&Math.abs(this.heights[l]-this.heights[a])<=v.STEP_M&&(e[l]=1,s.push(l))}}return n}buildingsOf(t,e,n){const s=this.width/2,a=this.height/2,o=[],r=[],i=[];for(const h of t){const M=h%this.width,c=Math.floor(h/this.width);o.push((M+.5-s)*e),r.push((a-c-.5)*e),i.push(this.heights[h])}return v.ofPoints(o,r,i,e,n)}static ofPoints(t,e,n,s,a,o=v.RECTANGLE_FILL,r=[]){const i=[];for(let d=0;d<t.length;d++)for(const[p,A]of[[-.5,-.5],[.5,-.5],[-.5,.5],[.5,.5]])i.push([t[d]+p*s,e[d]+A*s]);const h=v.hull(i),{angle:M,rectangle:c}=v.smallestRectangle(h),l=t.length*s*s,u=(c.maxU-c.minU)*(c.maxV-c.minV);if(l/u>=o||l<4*a)return[v.building(c,M,v.roof(n))];const f=h.map((d,p)=>({from:{east:d[0],north:d[1]},to:{east:h[(p+1)%h.length][0],north:h[(p+1)%h.length][1]}})),m=v.dominantAngles([...r,...f]);return v.parts(t,e,n,m.length>0?m:[M],s,a)}static dominantAngles(t,e=3){const n=new Map;for(const s of t){const a=Math.hypot(s.to.east-s.from.east,s.to.north-s.from.north);if(a<1e-6)continue;const o=(Math.atan2(s.to.north-s.from.north,s.to.east-s.from.east)%(Math.PI/2)+Math.PI/2)%(Math.PI/2),r=Math.round(o/(Math.PI/60));n.set(r,(n.get(r)??0)+a)}return[...n.entries()].sort((s,a)=>a[1]-s[1]).slice(0,e).map(([s])=>s*Math.PI/60)}static parts(t,e,n,s,a,o){let r=t.map((h,M)=>M);const i=[];for(let h=0;h<v.MAX_PARTS&&r.length>0;h++){let M;for(const u of s){const f=v.largestIn(t,e,r,u,a);f&&(M===void 0||f.area>M.area)&&(M=f)}if(M===void 0||M.area<o)break;const c=M.covered;Math.min(M.rectangle.maxU-M.rectangle.minU,M.rectangle.maxV-M.rectangle.minV)>=v.MIN_DEPTH_M&&i.push(v.building(M.rectangle,M.angle,v.roof([...c].map(u=>n[u])))),r=r.filter(u=>!c.has(u))}return i}static largestIn(t,e,n,s,a){const o=Math.cos(s),r=Math.sin(s);let i=1/0,h=-1/0,M=1/0,c=-1/0;const l=[],u=[];for(const D of n){const P=t[D]*o+e[D]*r,w=-t[D]*r+e[D]*o;l.push(P),u.push(w),i=Math.min(i,P),h=Math.max(h,P),M=Math.min(M,w),c=Math.max(c,w)}const f=Math.max(a*Math.SQRT2,Math.max(h-i,c-M)/v.MAX_CELLS),m=Math.max(1,Math.ceil((h-i)/f+1e-9)),d=Math.max(1,Math.ceil((c-M)/f+1e-9)),p=new Uint8Array(m*d),A=n.map((D,P)=>{const w=Math.min(m-1,Math.max(0,Math.floor((l[P]-i)/f))),O=Math.min(d-1,Math.max(0,Math.floor((u[P]-M)/f)));return p[O*m+w]=1,O*m+w}),E=v.largestRectangle(p,m,d);if(E===void 0)return;const b=new Set;return n.forEach((D,P)=>{const w=A[P]%m,O=Math.floor(A[P]/m);w>=E.column&&w<E.column+E.columns&&O>=E.row&&O<E.row+E.rows&&b.add(D)}),{area:E.area*f*f,angle:s,covered:b,rectangle:{minU:i+E.column*f,maxU:i+(E.column+E.columns)*f,minV:M+E.row*f,maxV:M+(E.row+E.rows)*f}}}static largestRectangle(t,e,n){const s=new Array(e).fill(0);let a;for(let o=0;o<n;o++){for(let i=0;i<e;i++)s[i]=t[o*e+i]?s[i]+1:0;const r=[];for(let i=0;i<=e;i++){const h=i===e?0:s[i];for(;r.length>0&&s[r[r.length-1]]>=h;){const M=r.pop(),c=r.length===0?i:i-r[r.length-1]-1,l=s[M]*c;(a===void 0||l>a.area)&&(a={column:i-c,row:o-s[M]+1,columns:c,rows:s[M],area:l})}r.push(i)}}return a}static roof(t){const e=[...t].sort((n,s)=>n-s);return Math.max(2,e[Math.min(e.length-1,Math.floor(e.length*v.ROOF_PERCENTILE))])}static building(t,e,n){const s=Math.cos(e),a=Math.sin(e),o=t.maxU-t.minU,r=t.maxV-t.minV,i=(t.minU+t.maxU)/2,h=(t.minV+t.maxV)/2,c=((90-(o<r?e:e+Math.PI/2)*180/Math.PI)%180+180)%180;return{eastM:v.tenth(i*s-h*a),northM:v.tenth(i*a+h*s),widthM:v.tenth(Math.max(o,r)),lengthM:v.tenth(Math.min(o,r)),heightM:v.tenth(n),headingDeg:Math.round(c*10)/10}}static smallestRectangle(t){let e={area:1/0,angle:0,rectangle:{minU:0,maxU:0,minV:0,maxV:0}};for(let n=0;n<Math.max(1,t.length);n++){const[s,a]=t[n]??[0,0],[o,r]=t[(n+1)%t.length]??[1,0],i=Math.atan2(r-a,o-s),h=Math.cos(i),M=Math.sin(i);let c=1/0,l=-1/0,u=1/0,f=-1/0;for(const[d,p]of t){const A=d*h+p*M,E=-d*M+p*h;c=Math.min(c,A),l=Math.max(l,A),u=Math.min(u,E),f=Math.max(f,E)}const m=(l-c)*(f-u);m<e.area&&(e={area:m,angle:i,rectangle:{minU:c,maxU:l,minV:u,maxV:f}})}return e}static hull(t){const e=[...t].sort((r,i)=>r[0]-i[0]||r[1]-i[1]),n=e.filter((r,i)=>i===0||r[0]!==e[i-1][0]||r[1]!==e[i-1][1]);if(n.length<3)return n;const s=(r,i,h)=>(i[0]-r[0])*(h[1]-r[1])-(i[1]-r[1])*(h[0]-r[0]),a=[];for(const r of n){for(;a.length>=2&&s(a[a.length-2],a[a.length-1],r)<=0;)a.pop();a.push(r)}const o=[];for(const r of[...n].reverse()){for(;o.length>=2&&s(o[o.length-2],o[o.length-1],r)<=0;)o.pop();o.push(r)}return[...a.slice(0,-1),...o.slice(0,-1)]}static tenth(t){return Math.round(t*10)/10}}class N{constructor(t,e,n,s){this.heights=t,this.vegetation=e,this.width=n,this.height=s}static SUMMIT_RADIUS_M=2.5;static CROWN_REACH_M=8;static MAX_WIDTH_M=16;static MIN_WIDTH_M=2;static tenth(t){return Math.round(t*10)/10}trees(t){const e=t.metresPerPixel,n=t.minHeightM??4,s=t.eyeM??1.6,a=t.minElevationDeg??2,o=Math.max(1,Math.round(N.SUMMIT_RADIUS_M/e)),r=Math.round(N.CROWN_REACH_M/e),i=this.width/2,h=this.height/2,M=[];for(let c=0;c<this.height;c++)for(let l=0;l<this.width;l++){const u=this.canopy(l,c);if(u<n)continue;const f=(l+.5-i)*e,m=(h-c-.5)*e,d=Math.hypot(f,m);if(d>t.radiusM||!this.isSummit(l,c,u,o))continue;const p=Math.atan2(u-s,Math.max(d,1))*180/Math.PI;p<a||M.push({tree:{eastM:N.tenth(f),northM:N.tenth(m),heightM:N.tenth(u),widthM:N.tenth(this.crownWidth(l,c,u,e,r))},elevation:p})}return M.sort((c,l)=>l.elevation-c.elevation),M.slice(0,t.maxTrees??150).map(c=>c.tree)}canopy(t,e){if(t<0||e<0||t>=this.width||e>=this.height)return 0;const n=e*this.width+t;return this.vegetation[n]?this.heights[n]:0}isSummit(t,e,n,s){for(let a=-s;a<=s;a++)for(let o=-s;o<=s;o++){if(o===0&&a===0)continue;const r=this.canopy(t+o,e+a);if(r>n||r===n&&(a<0||a===0&&o<0))return!1}return!0}crownWidth(t,e,n,s,a){let o=0;for(let i=-a;i<=a;i++)for(let h=-a;h<=a;h++)Math.hypot(h,i)*s<=N.CROWN_REACH_M&&this.canopy(t+h,e+i)>=n*.5&&o++;const r=2*Math.sqrt(o*s*s/Math.PI);return Math.max(N.MIN_WIDTH_M,Math.min(r,N.MAX_WIDTH_M))}}class x{static WALLS={1:"stone",2:"meuliere",3:"concrete",4:"brick",5:"block",6:"wood",9:"other"};static ROOFS={1:"tile",2:"slate",3:"zinc",4:"concrete",9:"other"};static WALL_COLOURS={stone:"#b9ae9b",meuliere:"#b8a78a",concrete:"#b4b2ac",brick:"#a65f48",block:"#d0c6b2",wood:"#7d5c3e",other:"#c9c0ae"};static PLASTER="#d3cab8";static ROOF_COLOURS={tile:"#a0583f",slate:"#4a5059",zinc:"#8d969c",concrete:"#8f8e8a",other:"#7a7872"};static VARIATION=.12;static of(t,e){const n=String(t??"").trim();if(!/^\d{1,2}$/.test(n))return;const s=Number(n),a=s%10!==0?s%10:Math.floor(s/10);return(e==="wall"?x.WALLS:x.ROOFS)[String(a)]}static roofColor(t){return t?x.ROOF_COLOURS[t]:void 0}static fit(t){return t.map(e=>{const n={color:x.wallColor(e)};return{...e,facade:n}})}static wallColor(t){const e=t.wallMaterial?x.WALL_COLOURS[t.wallMaterial]:x.PLASTER,n=Math.sin(t.eastM*12.9898+t.northM*78.233)*43758.5453,s=1+(n-Math.floor(n)-.5)*2*x.VARIATION,a=o=>Math.max(0,Math.min(255,Math.round(parseInt(e.slice(o,o+2),16)*s))).toString(16).padStart(2,"0");return`#${a(1)}${a(3)}${a(5)}`}}class g{static MIN_RISE_M=1.2;static MIN_EAVES_M=2.5;static MIN_SEEN=.35;static NO_DATA=-9999;static BANDS=7;static INSET_M=.9;static LIFT=1.18;static PEAK=.3;static LEVEL=.45;static fit(t,e,n){return!e&&!n&&!t.some(s=>s.roofMaterial)?t:t.map(s=>{const a=e?g.shapeOf(s,e):void 0,o=(n?g.colorOf(s,n):void 0)??x.roofColor(s.roofMaterial);if(!a&&!o)return s;const r={shape:a?.shape??"flat",...a?.ridge==="depth"?{ridge:"depth"}:{},riseM:a?.riseM??0,...o?{color:o}:{}},i=a&&a.shape!=="flat"?a.topM:s.heightM;return{...s,heightM:i,roof:r}})}static world(t,e,n){const s=t.headingDeg*Math.PI/180;return{east:t.eastM+e*Math.cos(s)+n*Math.sin(s),north:t.northM-e*Math.sin(s)+n*Math.cos(s)}}static shapeOf(t,e){const n=g.BANDS,s=Math.max(e.metresPerPixel,.5),a=t.widthM/2-g.INSET_M,o=t.lengthM/2-g.INSET_M;if(a<=0||o<=0)return;const r=[],i=Array.from({length:n},()=>({sum:0,n:0})),h=Array.from({length:n},()=>({sum:0,n:0}));let M=0;for(let P=-a;P<=a;P+=s)for(let w=-o;w<=o;w+=s){M++;const{east:O,north:C}=g.world(t,P,w),S=Math.floor(O/e.metresPerPixel+e.size/2),U=Math.floor(e.size/2-C/e.metresPerPixel);if(S<0||U<0||S>=e.size||U>=e.size)continue;const _=U*e.size+S,R=e.heights[_];if(e.vegetation[_]||R===g.NO_DATA||!(R>0))continue;r.push(R);const z=i[Math.min(n-1,Math.floor((P+a)/(2*a)*n))],W=h[Math.min(n-1,Math.floor((w+o)/(2*o)*n))];z.sum+=R,z.n++,W.sum+=R,W.n++}if(M===0||r.length/M<g.MIN_SEEN)return;r.sort((P,w)=>P-w);const c=g.percentile(r,.08),l=g.percentile(r,.92),u=l-c;if(c<g.MIN_EAVES_M||u>Math.min(t.widthM,t.lengthM))return;if(u<g.MIN_RISE_M)return{shape:"flat",riseM:0,topM:l};const f=g.profile(i),m=g.profile(h);if(!f||!m)return;const d=g.peak(m)/u,p=g.peak(f)/u,A=g.spread(f)/u,E=g.spread(m)/u,b=d>=g.PEAK&&A<=g.LEVEL,D=p>=g.PEAK&&E<=g.LEVEL;if(b&&(!D||d>=p))return{shape:"gable",ridge:"width",riseM:u,topM:l};if(D)return{shape:"gable",ridge:"depth",riseM:u,topM:l};if(d>=g.PEAK&&p>=g.PEAK/2)return{shape:"hip",ridge:"width",riseM:u,topM:l}}static profile(t){const e=t.map(n=>n.n>0?n.sum/n.n:NaN);return e.filter(Number.isFinite).length>=t.length-1?e.map((n,s)=>Number.isFinite(n)?n:g.neighbour(e,s)):void 0}static neighbour(t,e){return Number.isFinite(t[e-1])?t[e-1]:t[e+1]}static peak(t){const e=Math.max(1,Math.floor(t.length/3)),n=a=>a.reduce((o,r)=>o+r,0)/a.length,s=[...t.slice(0,e),...t.slice(t.length-e)];return n(t.slice(e,t.length-e))-n(s)}static spread(t){return Math.max(...t)-Math.min(...t)}static percentile(t,e){return t[Math.min(t.length-1,Math.floor(t.length*e))]}static colorOf(t,e){const n=Math.max(e.metresPerPixel,.5),s=Math.max(t.widthM/2-g.INSET_M,n),a=Math.max(t.lengthM/2-g.INSET_M,n),o=[],r=[],i=[];for(let c=-s;c<=s;c+=n)for(let l=-a;l<=a;l+=n){const{east:u,north:f}=g.world(t,c,l),m=Math.floor(u/e.metresPerPixel+e.size/2),d=Math.floor(e.size/2-f/e.metresPerPixel);if(m<0||d<0||m>=e.size||d>=e.size)continue;const p=(d*e.size+m)*e.samples,A=e.values[p],E=e.values[p+1],b=e.values[p+2];E>A&&E>b||(o.push(A),r.push(E),i.push(b))}if(o.length===0)return;const h=c=>c.sort((l,u)=>l-u)[Math.floor(c.length/2)],M=c=>Math.min(255,Math.round(c*g.LIFT)).toString(16).padStart(2,"0");return`#${M(h(o))}${M(h(r))}${M(h(i))}`}}class I{static DEFAULT_HEIGHT_M=6;static METRES_PER_DEGREE=111320;static STREET_SPACING_M=35;static KERB_OFFSET_M=1;static DEFAULT_WIDTH_M=5.5;static PATIENCE_MS=15e3;static MAX=150;client;constructor(t={}){this.client=new X(t)}static today(){return new Date().toISOString().slice(0,10)}async near(t,e){const n=e/I.METRES_PER_DEGREE,s=e/(I.METRES_PER_DEGREE*Math.cos(t.lat*Math.PI/180)),a=[t.lat-n,t.lng-s,t.lat+n,t.lng+s].map(i=>i.toFixed(6)).join(",");let o;const r=new Promise(i=>{o=setTimeout(()=>i(void 0),I.PATIENCE_MS)});try{const i=await Promise.race([this.client.query(`[out:json][timeout:25];(node["highway"="street_lamp"](${a});way["highway"]["lit"="yes"](${a}););out geom;`),r]);return i?I.lampsFrom(i,t,e):[]}catch{return[]}finally{clearTimeout(o)}}static lampsFrom(t,e,n){const s=[];for(const o of t.elements??[]){if(o.type==="way"){I.alongStreet(o,e,s);continue}if(typeof o.lat!="number"||typeof o.lon!="number")continue;const r=(o.lon-e.lng)*I.METRES_PER_DEGREE*Math.cos(e.lat*Math.PI/180),i=(o.lat-e.lat)*I.METRES_PER_DEGREE;if(Math.hypot(r,i)>n)continue;const h=Number.parseFloat(o.tags?.height??"");s.push({eastM:Math.round(r*10)/10,northM:Math.round(i*10)/10,...Number.isFinite(h)&&h>=2&&h<=20?{heightM:h}:{}})}const a=s.filter(o=>Math.hypot(o.eastM,o.northM)<=n);return s.length=0,s.push(...a),s.sort((o,r)=>Math.hypot(o.eastM,o.northM)-Math.hypot(r.eastM,r.northM)),s.slice(0,I.MAX)}static alongStreet(t,e,n){const s=t.geometry;if(!s||s.length<2)return;const a=f=>[(f.lon-e.lng)*I.METRES_PER_DEGREE*Math.cos(e.lat*Math.PI/180),(f.lat-e.lat)*I.METRES_PER_DEGREE],o=s.map(a),r=Number.parseFloat(t.tags?.width??""),i=Number.parseInt(t.tags?.lanes??"",10),M=(Number.isFinite(r)&&r>0?r:Number.isFinite(i)&&i>0?i*3.5:I.DEFAULT_WIDTH_M)/2+I.KERB_OFFSET_M;let c=1,l=I.STREET_SPACING_M/2,u=0;for(let f=1;f<o.length;f++){const[m,d]=o[f-1],[p,A]=o[f],E=Math.hypot(p-m,A-d);if(E===0)continue;const b=(p-m)/E,D=(A-d)/E;for(;l<=u+E;l+=I.STREET_SPACING_M){const P=l-u;n.push({eastM:Math.round((m+b*P+D*M*c)*10)/10,northM:Math.round((d+D*P-b*M*c)*10)/10}),c=-c}u+=E}}}class k{id="ign-lidar-hd";name="IGN LiDAR HD";attribution="Canopy and roofs © IGN, LiDAR HD (Licence Ouverte 2.0), as surveyed on a day the service does not give";creditUrl="https://geoservices.ign.fr/lidarhd";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new T(t),this.lamps=e}covers(t,e){return T.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new F("no-coverage","The LiDAR HD survey covers France only");const n=T.lidarRadius(e),[s,a,o,r,i]=await Promise.all([this.platform.lidar(t,n),this.platform.photo(t,n).catch(()=>{}),this.lamps?.near(t,e),this.platform.laserDate(t),this.platform.photoDate(t)]);return{trees:new N(s.heights,s.vegetation,s.size,s.size).trees({metresPerPixel:s.metresPerPixel,radiusM:n}),buildings:x.fit(g.fit(new v(s.heights,s.building,s.size,s.size).buildings({metresPerPixel:s.metresPerPixel,radiusM:n,maxBuildings:300}),s,a)),bridges:[],lines:[],...o?{lamps:o}:{},dates:{...r?{laser:r}:{},...a&&i?{photo:i}:{},...o?.length?{lamps:I.today()}:{}}}}}class y{static METRES_PER_DEGREE=111320;static DEFAULT_HEIGHT_M=6;static FLOOR_HEIGHT_M=3;static FINE_M=.5;static COARSE_M=1;static MAX_CELLS=4e4;static RECTANGLE_FILL=.9;static buildings(t,e,n,s=12,a=300){const o=[];for(const r of t){const i=y.rings(r,e);if(i.length===0)continue;const h=Number(r.properties.nombre_d_etages),M=Number(r.properties.hauteur),c=Number.isFinite(M)&&M>0?M:Number.isFinite(h)&&h>0?h*y.FLOOR_HEIGHT_M:y.DEFAULT_HEIGHT_M;for(const l of y.rectanglesOf(i,c,s)){if(Math.hypot(l.eastM,l.northM)>n)continue;const u=x.of(r.properties.materiaux_des_murs,"wall"),f=x.of(r.properties.materiaux_de_la_toiture,"roof");o.push({...l,...Number.isFinite(h)&&h>0?{floors:h}:{},...u?{wallMaterial:u}:{},...f?{roofMaterial:f}:{}})}}return o.sort((r,i)=>Math.hypot(r.eastM,r.northM)-Math.hypot(i.eastM,i.northM)),o.slice(0,a)}static named(t,e,n){const s=e.map(a=>({name:typeof a.properties.toponyme=="string"?a.properties.toponyme:"",rings:y.rings(a,n)})).filter(a=>a.name.length>0&&a.rings.length>0);return s.length===0?t:t.map(a=>{const o=s.find(r=>y.inside(a.eastM,a.northM,r.rings));return o?{...a,name:o.name}:a})}static inside(t,e,n){let s=0;for(const a of n)for(let o=0;o+1<a.length;o++){const r=a[o],i=a[o+1];r.north<=e!=i.north<=e&&r.east+(e-r.north)/(i.north-r.north)*(i.east-r.east)>t&&s++}return s%2===1}static rings(t,e){const n=t.geometry,s=n.type==="Polygon"?[n.coordinates]:n.type==="MultiPolygon"?n.coordinates:[],a=Math.cos(e.lat*Math.PI/180),o=[];for(const r of s)for(const i of r)o.push(i.map(([h,M])=>({east:(h-e.lng)*y.METRES_PER_DEGREE*a,north:(M-e.lat)*y.METRES_PER_DEGREE})));return o}static rectanglesOf(t,e,n){let s=1/0,a=-1/0,o=1/0,r=-1/0;for(const l of t)for(const u of l)s=Math.min(s,u.east),a=Math.max(a,u.east),o=Math.min(o,u.north),r=Math.max(r,u.north);let i=y.FINE_M;(a-s)*(r-o)/(i*i)>y.MAX_CELLS&&(i=y.COARSE_M);const h=[],M=[];for(let l=o+i/2;l<r;l+=i){const u=[];for(const f of t)for(let m=0;m+1<f.length;m++){const d=f[m],p=f[m+1];d.north<=l!=p.north<=l&&u.push(d.east+(l-d.north)/(p.north-d.north)*(p.east-d.east))}u.sort((f,m)=>f-m);for(let f=0;f+1<u.length;f+=2)for(let m=Math.ceil((u[f]-i/2)/i)*i+i/2;m<u[f+1];m+=i)h.push(m),M.push(l)}if(h.length*i*i<n)return[];const c=t.flatMap(l=>l.slice(0,-1).map((u,f)=>({from:u,to:l[f+1]})));return v.ofPoints(h,M,new Array(h.length).fill(e),i,n,y.RECTANGLE_FILL,c)}}class ${id="ign-lidar-topo";name="IGN LiDAR HD + BD TOPO";attribution="Canopy © IGN, LiDAR HD; buildings, bridges and lines © IGN, BD TOPO (Licence Ouverte 2.0), as surveyed on days the services do not give";creditUrl="https://geoservices.ign.fr/lidarhd";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new T(t),this.lamps=e}covers(t,e){return T.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new F("no-coverage","The IGN's surveys cover France only");const n=T.radius(e),[s,a,o,r,i,h]=await Promise.allSettled([this.platform.lidar(t,n),this.platform.vectors(t,n,!0),this.platform.photo(t,n),this.lamps?.near(t,e)??Promise.resolve(void 0),this.platform.laserDate(t),this.platform.photoDate(t)]),M=T.lidarRadius(n);if(s.status==="rejected"&&a.status==="rejected")throw s.reason;const c=s.status==="fulfilled"?s.value:void 0,l=a.status==="fulfilled"?a.value:void 0;return{trees:c?new N(c.heights,c.vegetation,c.size,c.size).trees({metresPerPixel:c.metresPerPixel,radiusM:M}):[],buildings:this.roofed(this.buildings(c,l,t,n),c,o.status==="fulfilled"?o.value:void 0),bridges:l?L.bridges(l,t,n,c?.structures):[],lines:l?L.lines(l,t,n,c?.structures):[],...r.status==="fulfilled"&&r.value?{lamps:r.value}:{},dates:{...s.status==="fulfilled"&&i.status==="fulfilled"&&i.value?{laser:i.value}:{},...o.status==="fulfilled"&&h.status==="fulfilled"&&h.value?{photo:h.value}:{},...l?.mapDate?{map:l.mapDate}:{},...r.status==="fulfilled"&&r.value?.length?{lamps:I.today()}:{}}}}roofed(t,e,n){return x.fit(g.fit(t,e,n))}buildings(t,e,n,s){const a=e?y.buildings(e.buildings,n,s):[],o=a.length>0||t===void 0?a:new v(t.heights,t.building,t.size,t.size).buildings({metresPerPixel:t.metresPerPixel,radiusM:T.lidarRadius(s),maxBuildings:300});return e?y.named(o,e.zones,n):o}}class K{id="ign-bd-topo";name="IGN BD TOPO";attribution="Buildings, bridges and lines © IGN, BD TOPO (Licence Ouverte 2.0), as drawn on a day the service does not give";creditUrl="https://geoservices.ign.fr/bdtopo";contemporary=!0;platform;lamps;constructor(t={},e){this.platform=new T(t),this.lamps=e}covers(t,e){return T.covers(t,e)}async getSurroundings(t,e){if(!this.covers(t.lat,t.lng))throw new F("no-coverage","BD TOPO covers France only");const n=T.radius(e),[s,a,o,r]=await Promise.all([this.platform.vectors(t,n,!0),this.platform.photo(t,n).catch(()=>{}),this.lamps?.near(t,e),this.platform.photoDate(t)]);return{trees:[],buildings:x.fit(g.fit(y.named(y.buildings(s.buildings,t,n),s.zones,t),void 0,a)),bridges:L.bridges(s,t,n,void 0),lines:L.lines(s,t,n,void 0),...o?{lamps:o}:{},dates:{...s.mapDate?{map:s.mapDate}:{},...a&&r?{photo:r}:{},...o?.length?{lamps:I.today()}:{}}}}}const G=[{id:"ign-lidar-topo",name:"IGN LiDAR HD + BD TOPO",credit:"© IGN, LiDAR HD and BD TOPO, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/lidarhd",create:()=>new $({},new I)},{id:"ign-lidar-hd",name:"IGN LiDAR HD",credit:"© IGN, LiDAR HD, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/lidarhd",create:()=>new k({},new I)},{id:"ign-bd-topo",name:"IGN BD TOPO",credit:"© IGN, BD TOPO, Licence Ouverte 2.0; street lamps © OpenStreetMap contributors",creditUrl:"https://geoservices.ign.fr/bdtopo",create:()=>new K({},new I)}];class H{static DEFAULT_ID=G[0].id;static NONE="none";static byId(t){return t===H.NONE?void 0:G.find(e=>e.id===t)??G[0]}static providerFor(t,e,n){const s=H.byId(t)?.create();return s?.covers(e,n)?s:void 0}}export{G as SURROUNDINGS_SOURCES,H as SurroundingsSources};