1/* Screenshot generator za karte parcel/stavb iz koÅ¡arice â samostojen modul, ki ga 2 kliÄe izvoz poroÄila (export.js, žiÄno kasneje). GURS WMS zna sam server-side 3 renderirati PNG (GetMap), zato ni potreben headless brskalnik ali Leaflet-canvas trik. 4 Namenoma NE uporablja satelitskih Esri tile-ov (drugje v map.js) â licenca za vgradnjo 5 v statiÄen/redistribuiran izvoz ni razjasnjena. Za letalski posnetek zato uporabimo 6 uradni državni ortofoto DOF025 (25 cm/px, © GURS, javni podatki, CORS odprt) z 7 loÄenega WMS endpointa (dts) â ker en GetMap ne zna meÅ¡ati slojev z dveh storitev, 8 se ortofoto+parcele sestavi client-side na canvasu (fetchComposite). 9 Pogleda potres/plazovi dodajata javna sloja nevarnosti: ARSO karta potresne 10 nevarnosti 2021 (prek /arso-wms proxyja v worker.js, ker ARSO nima CORS) in 11 GeoZS verjetnost pojavljanja plazov (direktno, CORS odprt). */ 12 13import { parcelaGeom, stavbaGeom, namenskeRabeBbox, vir } from "./gurs.js?v=281"; 14import { izvozRacunEmail } from "./export.js?v=281"; // podpis raÄuna v vrstici z datumom 15 16export const WMS_URL = vir("/wms-kn", "https://ipi.eprostor.gov.si/wms-si-gurs-kn/wms"); 17export const DTS_URL = vir("/wms-dts", "https://ipi.eprostor.gov.si/wms-si-gurs-dts/wms"); 18// krovna GURS WMS storitev â edina, ki streže tudi GJI (zbirni kataster gospodarske 19// javne infrastrukture: vodovod, kanalizacija, elektrika, plin ...). CORS odprt. 20export const GJI_URL = vir("/wms-gji", "https://ipi.eprostor.gov.si/wms-si-gurs/wms"); 21// ARSO geoserver ne poÅ¡ilja CORS glav -> gre prek proxyja v worker.js (same-origin). 22// GeoZS geoserver ima CORS odprt (Access-Control-Allow-Origin: *), gre direktno. 23export const ARSO_URL = "/arso-wms"; 24export const GEOZS_URL = "https://geoserver.geo-zs.si/pub-ins-data/nz/wms"; 25// MNVP zbirka prostorskih aktov (EUP) â isti host kot GURS WMS-ji zgoraj, CORS odprt, direktno. 26export const MNVP_URL = vir("/wms-mnvp", "https://ipi.eprostor.gov.si/wms-si-mnvp-pa/wms"); 27 28/* Zbledela ortofoto podlaga pod tematskimi pogledi: dovolj, da se vidi realnost 29 (strehe, ceste), a ne preglasi slojev nad njo. maxGSD omeji zahtevano loÄljivost 30 DOF-a na ~2x native (25 cm/px): pri veÄji poveÄavi bi server zaradi 31 MinScaleDenominator tiho vrnil PRAZNO sliko (že prizadelo uporabnika pri 32 "visoki" loÄljivosti), poleg tega finejÅ¡i piksli ne prinesejo niÄ. */ 33const DOF_PODLAGA = { url: DTS_URL, layers: ["SI.GURS.ZPDZ:DOF025"], opacity: 0.45, maxGSD: 0.125 }; 34/* pxCap: linijske sloje (kataster, GJI) zahtevaj pri najveÄ ~1300 px in raztegni â 35 WMS riÅ¡e Ärte in napise v fiksnih pikslih, zato so bili na 2400 px slikah 36 pretanki, katastrske Å¡tevilke pa premajhne; z raztegom postanejo ~2x izrazitejÅ¡i. */ 37const PARCELE_CREZ = { url: WMS_URL, layers: ["SI.GURS.KN:PARCELE"], pxCap: 1300 }; 38 39/** Nabori WMS slojev za posamezen "pogled" (prvi vir v seznamu = spodaj). 40 Vnos je lahko seznam imen slojev (en endpoint, WMS_URL) ali seznam 41 {url, layers, opacity?, maxGSD?} virov za client-side kompozit (razliÄni 42 endpointi; opacity < 1 nariÅ¡e vir polprosojno, maxGSD [m/px] omeji zahtevano 43 loÄljivost rastra). */ 44/* Tematski pogledi (bonitete/rabe/nevarnosti/GJI) NAMENOMA brez GURS sloja PARCELE: 45 zelene GURS meje so na tematskih ploskvah slabo vidne, na GJI pa se tolÄejo z 46 zeleno elektriko. Meje nariÅ¡e orodje samo (opts.mejeGeoms iz WFS) â tanke sive 47 z belim robom, berljive na vsaki podlagi. */ 48export const LAYER_SETS = { 49 ortofoto: [{ url: DTS_URL, layers: ["SI.GURS.ZPDZ:DOF025"], maxGSD: 0.125 }, PARCELE_CREZ], 50 kataster: [DOF_PODLAGA, 51 { url: WMS_URL, layers: ["SI.GURS.KN:PARCELE", "SI.GURS.KN:STAVBE_OBRIS", "SI.GURS.KN:OSNOVNI_PARCELE_CENTROID"], pxCap: 1300 }], 52 bonitete: [DOF_PODLAGA, { url: WMS_URL, layers: ["SI.GURS.KN:BONITETE"], opacity: 0.65, pxCap: 1300 }], 53 raba: [DOF_PODLAGA, { url: WMS_URL, layers: ["SI.GURS.KN:DEJANSKE_RABE"], opacity: 0.65 }], 54 namenska: [DOF_PODLAGA, { url: WMS_URL, layers: ["SI.GURS.KN:NAMENSKE_RABE"], opacity: 0.65 }], 55 // prostorski plan za obÄine: namenska raba (barvne cone) + EUP oznake/meje nad njo 56 plan: [DOF_PODLAGA, { url: WMS_URL, layers: ["SI.GURS.KN:NAMENSKE_RABE"], opacity: 0.65 }, 57 { url: MNVP_URL, layers: ["SI.MNVP.PA:EUP_OPN"] }], 58 potres: [DOF_PODLAGA, { url: ARSO_URL, layers: ["ARSO:POTRS_NEVAR_KRT_2021"], opacity: 0.55 }],
59 plazovi: [DOF_PODLAGA, { url: GEOZS_URL, layers: ["nz:ver_plaz"], opacity: 0.55 }], 60 // poplave: državnega WMS ni â razredi IKRPN se nariÅ¡ejo client-side 61 // (poplave.js, overlayDraw v slikaPogleda), tu je samo podlaga 62 poplave: [DOF_PODLAGA], 63 // promet: DRAJV dogodki tvegane vožnje se nariÅ¡ejo client-side, enako kot 64 // poplave (drajv.js, overlayDraw v slikaPogleda), tu je samo podlaga 65 promet: [DOF_PODLAGA], 66 // poplavni dosegi Q10/Q100/Q500 in globine pri Q100 (DRSV IKPN/IKG, ArcGIS export prek /drsv) 67 q10: [DOF_PODLAGA, { url: "/drsv/IKPN/MapServer/export", arcgis: true, layers: ["4006"] }], 68 q100: [DOF_PODLAGA, { url: "/drsv/IKPN/MapServer/export", arcgis: true, layers: ["4007"] }], 69 q500: [DOF_PODLAGA, { url: "/drsv/IKPN/MapServer/export", arcgis: true, layers: ["4008"] }], 70 globina: [DOF_PODLAGA, { url: "/drsv/IKG/MapServer/export", arcgis: true, layers: ["4003", "4002", "4001"] }], 71 // vrstni red GJI: el. komunikacije spodaj (teÄejo v istih koridorjih kot elektrika 72 // in so jo prej prekrivale), elektrika NA VRHU â vsa omrežja ostanejo vidna. 73 // maxGSD 0.025: GJI sloji imajo MinScaleDenominator 50 â pri zelo majhni parceli 74 // s tesnim odmikom bi zahteva pod 1:50 vrnila prazno; omejitev px to prepreÄi 75 komunalni: [DOF_PODLAGA, 76 { url: GJI_URL, layers: ["SI.GURS.KGI:LINIJE_ELEKTRONSKE_KOMUNIKACIJE_G", "SI.GURS.KGI:LINIJE_TOPLOTNA_ENERGIJA_G", "SI.GURS.KGI:LINIJE_ZEMELJSKI_PLIN_G", "SI.GURS.KGI:LINIJE_KANALIZACIJA_G", "SI.GURS.KGI:LINIJE_VODOVOD_G", "SI.GURS.KGI:LINIJE_ELEKTRICNA_ENERGIJA_G"], pxCap: 1300, maxGSD: 0.025 }], 77}; 78 79const gjiLeg = (layer, napis) => ({ 80 url: GJI_URL + "?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image/png&layer=" + layer, 81 napis, 82}); 83 84/** Legende za poglede, kjer slika brez njih ni berljiva. Vrednost je bodisi en 85 GetLegendGraphic URL (server vrne celo legendo z napisi) bodisi seznam 86 {url, napis} â GJI legende so samo 20x20 simboli brez besedila, zato se napisi 87 doriÅ¡ejo pri nas. Uporabi jih slikaPogleda prek opts.legendUrl. */ 88const knLeg = (layer) => WMS_URL + "?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image/png&layer=" + layer; 89 90/** GJI omrežja (za legendo in preverjanje prisotnosti v izrezu). */ 91const GJI_OMREZJA = [ 92 { layer: "SI.GURS.KGI:LINIJE_VODOVOD_G", napis: "Vodovod" }, 93 { layer: "SI.GURS.KGI:LINIJE_KANALIZACIJA_G", napis: "Kanalizacija" }, 94 { layer: "SI.GURS.KGI:LINIJE_ELEKTRICNA_ENERGIJA_G", napis: "Elektrika" }, 95 { layer: "SI.GURS.KGI:LINIJE_ZEMELJSKI_PLIN_G", napis: "Plin" }, 96 { layer: "SI.GURS.KGI:LINIJE_TOPLOTNA_ENERGIJA_G", napis: "Toplotna energija" }, 97 { layer: "SI.GURS.KGI:LINIJE_ELEKTRONSKE_KOMUNIKACIJE_G", napis: "El. komunikacije" }, 98]; 99 100export const LEGENDS = { 101 bonitete: knLeg("SI.GURS.KN:BONITETE"), 102 raba: knLeg("SI.GURS.KN:DEJANSKE_RABE"), 103 namenska: knLeg("SI.GURS.KN:NAMENSKE_RABE"), 104 plan: knLeg("SI.GURS.KN:NAMENSKE_RABE"), 105 potres: ARSO_URL + "?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image/png&layer=ARSO:POTRS_NEVAR_KRT_2021", 106 plazovi: GEOZS_URL + "?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image/png&layer=nz:ver_plaz", 107 komunalni: GJI_OMREZJA.map((o) => gjiLeg(o.layer, o.napis)), 108 // barve = renderer DRSV servisov (alfa 153/255 nad ortofotom) 109 q10: [{ barva: "#005ce6", napis: "Poplavljeno pri pretoku Q10 (10-letne vode)" }], 110 q100: [{ barva: "#00a9e6", napis: "Poplavljeno pri pretoku Q100 (100-letne vode)" }], 111 q500: [{ barva: "#73ffdf", napis: "Poplavljeno pri pretoku Q500 (500-letne vode)" }], 112 globina: [{ barva: "#00a9e6", napis: "Globina vode 1,5 m in veÄ" }, { barva: "#73dfff", napis: "Globina 0,5 do 1,5 m" }, { barva: "#bee8ff", napis: "Globina manj kot 0,5 m" }], 113}; 114 115/** Ali je slika (WMS PNG) povsem prazna/prozorna â za zaznavo omrežij, ki jih v 116 izrezu ni (enak trik kot prazni letniki v dovoljenja.js). */ 117async function jePraznaSlika(blob) { 118 const img = await createImageBitmap(blob); 119 const c = document.createElement("canvas"); 120 c.width = img.width; c.height = img.height; 121 const ctx = c.getContext("2d", { willReadFrequently: true }); 122 ctx.drawImage(img, 0, 0); 123 const data = ctx.getImageData(0, 0, img.width, img.height).data; 124 for (let i = 3; i < data.length; i += 4) if (data[i] > 10) return false;
125 return true; 126} 127 128/** Legenda komunalnih vodov samo za omrežja, ki so v danem izrezu res prisotna â 129 sicer legenda obljublja plin/toploto, ki ju na sliki ni, kar bralca zavede. 130 Prisotnost preveri z majhnim GetMap na omrežje. POZOR: GJI sloji se riÅ¡ejo 131 samo v oknu merila 1:50-1:11000, zato mora biti sonda v istem oknu kot glavna 132 slika â fiksnih 200 px je bilo pri Å¡irÅ¡ih izrezih Äez 1:11000 in server je 133 vraÄal prazno, legenda pa je izpuÅ¡Äala narisana omrežja. Ob napaki vrstico 134 raje obdrži. */ 135export async function gjiLegendaZaBbox(bbox) { 136 const span = longSideMeters(normalizeBounds(bbox)); 137 const probePx = Math.min(Math.max(Math.round(span / 3.0), 120), 800); // ~1:10700 ali fineje 138 const vrstice = await Promise.all(GJI_OMREZJA.map(async (o) => { 139 try { 140 const blob = await fetchScreenshot(bbox, [o.layer], { url: GJI_URL, maxPx: probePx, minPx: 60 }); 141 return (await jePraznaSlika(blob)) ? null : gjiLeg(o.layer, o.napis); 142 } catch { return gjiLeg(o.layer, o.napis); } 143 })); 144 const prisotne = vrstice.filter(Boolean); 145 return prisotne.length ? prisotne : null; 146} 147 148// najmanjÅ¡i razpon bbox-a v stopinjah (~60-90 m pri slovenskih zemljepisnih Å¡irinah) â 149// brez tega bi zelo majhna parcela (npr. 90 m² etažni del) dobila bbox skoraj enak 150// robu parcele, kar po zaokrožitvi na piksle lahko da degenerirano (skoraj 1x1 px) sliko. 151const MIN_SPAN_DEG = 0.0008; 152 153// 1 stopinja zemljepisne dolžine/Å¡irine v km (za pretvorbo bbox -> realna razdalja na tleh) 154const KM_PER_DEG_LAT = 111.32; 155 156function toRad(deg) { return (deg * Math.PI) / 180; } 157 158/** Sprehodi vse koordinate [lon,lat] v GeoJSON Feature/Geometry (poljubna gnezditev). */ 159function eachCoord(geomOrFeature, fn) { 160 const geometry = geomOrFeature && geomOrFeature.type === "Feature" ? geomOrFeature.geometry : geomOrFeature; 161 if (!geometry) return; 162 if (geometry.type === "GeometryCollection") { 163 for (const g of geometry.geometries || []) eachCoord(g, fn); 164 return; 165 } 166 const depth = { Point: 0, MultiPoint: 1, LineString: 1, MultiLineString: 2, Polygon: 2, MultiPolygon: 3 }[geometry.type]; 167 if (depth === undefined) return; 168 const walk = (coords, d) => { if (d === 0) fn(coords); else for (const c of coords) walk(c, d - 1); }; 169 walk(geometry.coordinates, depth); 170} 171 172/** Bbox ene same geometrije, brez paddinga â uporabljeno za gruÄenje (center). */ 173function rawBbox(geom) { 174 let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity; 175 eachCoord(geom, ([lon, lat]) => { 176 if (lon < west) west = lon; 177 if (lon > east) east = lon; 178 if (lat < south) south = lat; 179 if (lat > north) north = lat; 180 }); 181 return { west, south, east, north }; 182} 183 184function centerOf(geom) { 185 const b = rawBbox(geom); 186 return { lat: (b.south + b.north) / 2, lon: (b.west + b.east) / 2 }; 187} 188 189function haversineKm(a, b) { 190 const R = 6371; 191 const dLat = toRad(b.lat - a.lat); 192 const dLon = toRad(b.lon - a.lon); 193 const la1 = toRad(a.lat), la2 = toRad(b.lat); 194 const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2; 195 return 2 * R * Math.asin(Math.sqrt(h)); 196} 197 198/** Zbere bbox ene ali veÄ GeoJSON geometrij, z odstotkovnim robom (padding). */ 199export function bboxOf(geoms, padRatio = 0.15) { 200 let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity; 201 for (const g of geoms) { 202 const b = rawBbox(g); 203 if (b.west < west) west = b.west; 204 if (b.east > east) east = b.east; 205 if (b.south < south) south = b.south; 206 if (b.north > north) north = b.north; 207 } 208 if (!Number.isFinite(west)) throw new Error("bboxOf: ni veljavnih geometrij"); 209 210 let width = east - west, height = north - south; 211 // padding kot % max stranice, a ne manj kot % MIN_SPAN_DEG (da tudi toÄkovna/niÄelna 212 // geometrija dobi razumen rob namesto paddinga 0) 213 const padW = Math.max(width, MIN_SPAN_DEG) * padRatio; 214 const padH = Math.max(height, MIN_SPAN_DEG) * padRatio; 215 west -= padW; east += padW; south -= padH; north += padH; 216 217 // po paddingu razteg na minimalni razpon, Äe je geometrija (skoraj) toÄka ali degenerirana Ärta 218 width = east - west; height = north - south; 219 if (width < MIN_SPAN_DEG) { const d = (MIN_SPAN_DEG - width) / 2; west -= d; east += d; } 220 if (height < MIN_SPAN_DEG) { const d = (MIN_SPAN_DEG - height) / 2; south -= d; north += d; } 221 222 return { south, west, north, east }; 223} 224 225/** Greedy/single-link gruÄenje po geografski bližini centrov geometrij: geometrije, ki 226 so si blizu (znotraj praga), pristanejo v isti gruÄi tudi posredno (A blizu B, B blizu C 227 => A,B,C ena gruÄa, Äeprav A-C morda malo presega prag) â to je namerno, ker gre za 228 "verigo" sosednjih parcel v istem naselju. Uporabljen union-find nad vsemi pari (O(n²), 229 dovolj za tipiÄno velikost koÅ¡arice). Privzeti prag 5 km: naselja/zaselki so tipiÄno 230 znotraj tega premera, sosednje obÄine/regije pa obiÄajno dlje. */
231export function clusterGeoms(geoms, thresholdKm = 5) { 232 const n = geoms.length; 233 const centers = geoms.map(centerOf); 234 const parent = Array.from({ length: n }, (_, i) => i); 235 const find = (i) => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; }; 236 const union = (a, b) => { const ra = find(a), rb = find(b); if (ra !== rb) parent[ra] = rb; }; 237 238 for (let i = 0; i < n; i++) { 239 for (let j = i + 1; j < n; j++) { 240 if (haversineKm(centers[i], centers[j]) <= thresholdKm) union(i, j); 241 } 242 } 243 244 const groups = new Map(); 245 for (let i = 0; i < n; i++) { 246 const r = find(i); 247 if (!groups.has(r)) groups.set(r, []); 248 groups.get(r).push(geoms[i]); 249 } 250 return [...groups.values()]; 251} 252 253function normalizeBounds(bounds) { 254 if (bounds && typeof bounds.getSouth === "function") { 255 return { south: bounds.getSouth(), west: bounds.getWest(), north: bounds.getNorth(), east: bounds.getEast() }; 256 } 257 const { south, west, north, east } = bounds; 258 return { south, west, north, east }; 259} 260 261/* Piksel dimenzije iz bbox-a. GetMap kliÄemo v CRS=EPSG:4326 (= surove stopinje kot 262 karteziÄne koordinate, "Plate Carrée"), zato 1° zemljepisne dolžine na tleh predstavlja 263 manj km kot 1° zemljepisne Å¡irine (razmerje = cos(zemljepisna Å¡irina), v SLO ok. 0.69). 264 Äe bi Å¡irino/viÅ¡ino slike raÄunali kar iz surovega razmerja stopinj, bi bile parcele na 265 sliki vodoravno raztegnjene (krog bi izpadel kot elipsa). Zato razmerje popravimo na 266 realno razdaljo na tleh, dolga stranica pa je omejena na `maxPx`. */ 267function pixelSize(bounds, maxPx = 1200, minPx = 100) { 268 const { south, west, north, east } = bounds; 269 const latMid = (south + north) / 2; 270 const lonSpanKm = (east - west) * KM_PER_DEG_LAT * Math.cos(toRad(latMid)); 271 const latSpanKm = (north - south) * KM_PER_DEG_LAT; 272 let width, height; 273 if (lonSpanKm >= latSpanKm) { 274 width = maxPx; 275 height = Math.round(maxPx * (latSpanKm / lonSpanKm)); 276 } else { 277 height = maxPx; 278 width = Math.round(maxPx * (lonSpanKm / latSpanKm)); 279 } 280 return { width: Math.max(width, minPx), height: Math.max(height, minPx) }; 281} 282 283/** Sestavi WMS GetMap URL za dano bbox (Leaflet LatLngBounds ali {south,west,north,east}) 284 in seznam WMS layer imen (v vrstnem redu risanja, prvi=spodaj). Avtomatsko izraÄuna 285 width/height iz razmerja bbox-a (dolga stranica max ~1200px), format=image/png, 286 transparent=true. */ 287export function getMapUrl(bounds, wmsLayers, opts = {}) { 288 const b = normalizeBounds(bounds); 289 const { width, height } = pixelSize(b, opts.maxPx ?? 2400, opts.minPx ?? 100); 290 // ArcGIS REST export (DRSV prek /drsv proxyja): bbox je lon,lat; isti canvas kot WMS 291 if (opts.arcgis) return opts.url + "?" + new URLSearchParams({ 292 bbox: `${b.west},${b.south},${b.east},${b.north}`, bboxSR: "4326", imageSR: "4326", 293 size: `${width},${height}`, layers: "show:" + wmsLayers.join(","), format: "png32", transparent: "true", f: "image", 294 }); 295 // WMS 1.3.0 + EPSG:4326 => avtoritetni vrstni red osi je lat,lon (enako kot pri WFS bbox v gurs.js) 296 const bbox = `${b.south},${b.west},${b.north},${b.east}`; 297 const params = new URLSearchParams({ 298 service: "WMS", 299 version: "1.3.0", 300 request: "GetMap", 301 layers: wmsLayers.join(","), 302 styles: wmsLayers.map(() => "").join(","), 303 crs: "EPSG:4326", 304 bbox, 305 width: String(width), 306 height: String(height), 307 format: "image/png", 308 transparent: "true", 309 }); 310 // dodatni WMS parametri, npr. time za Äasovno dimenzijo (zgodovinski DOF050_Z) 311 if (opts.extraParams) for (const [k, v] of Object.entries(opts.extraParams)) params.set(k, v); 312 return (opts.url ?? WMS_URL) + "?" + params.toString(); 313} 314 315/** Prenese sliko (fetch -> Blob) za dano bbox+sloje. GURS WMS ob napaki (npr. neobstojeÄ 316 sloj, prevelika slika) pogosto vrne HTTP 200 z XML/ServiceException telesom namesto 317 slike â to eksplicitno preverimo prek content-type, da se ne vrne "slika", ki je v resnici XML. */ 318export async function fetchScreenshot(bounds, wmsLayers, opts = {}) { 319 const url = getMapUrl(bounds, wmsLayers, opts); 320 let r; 321 for (let poskus = 0; ; poskus++) { 322 // GURS gateway ob preobremenitvi OBVISI 30 s in Å¡ele nato vrne 504 â brez lastne 323 // prekinitve bi 3 poskusi vzeli ~95 s. Prekinemo po 12 s ÄAKANJA NA GLAVE (uspeÅ¡en 324 // render tudi velikih slik glave vrne prej); prenos telesa po glavah ni omejen. 325 const ctl = new AbortController(); 326 const timer = setTimeout(() => ctl.abort(), 12000); 327 r = await fetch(url, { ...opts.fetchOpts, signal: ctl.signal }) 328 .catch((e) => { if (poskus >= 2) throw new Error("GURS strežnik ne odgovarja (preobremenjen) â poskusi znova Äez minuto."); return null; }) 329 .finally(() => clearTimeout(timer)); 330 // GURS WMS je obÄasno preobremenjen (503/504 Gateway Timeout) â do 2 ponovna poskusa s premorom 331 if ((!r || r.status >= 500) && poskus < 2) { await new Promise((res) => setTimeout(res, 1500 * (poskus + 1)));
331 continue; } 332 break; 333 } 334 const ct = r.headers.get("content-type") || ""; 335 if (!r.ok || !ct.startsWith("image/")) { 336 // prevelika slika sproži ServiceException â poskusi Å¡e s prejÅ¡njo (nižjo) loÄljivostjo 337 if ((opts.maxPx ?? 2400) > 1200) return fetchScreenshot(bounds, wmsLayers, { ...opts, maxPx: 1200 }); 338 const text = await r.text().catch(() => ""); 339 throw new Error(`WMS GetMap napaka (${r.status} ${ct}): ${text.slice(0, 300)}`); 340 } 341 return r.blob(); 342} 343 344/** Dolžina daljÅ¡e stranice bbox-a v metrih (za omejitev loÄljivosti rastra). */ 345function longSideMeters(b) { 346 const latMid = (b.south + b.north) / 2; 347 const lonM = (b.east - b.west) * KM_PER_DEG_LAT * 1000 * Math.cos(toRad(latMid)); 348 const latM = (b.north - b.south) * KM_PER_DEG_LAT * 1000; 349 return Math.max(lonM, latM); 350} 351 352/** Kompozit iz veÄ WMS virov (razliÄni endpointi, npr. DOF ortofoto + parcele) v en PNG. 353 Vsi viri se riÅ¡ejo v isto velikost platna (iz bbox-a), spodnji vir prvi; vir z 354 maxGSD [m/px] se zahteva v omejeni loÄljivosti in raztegne (glej DOF_PODLAGA). 355 Ozadje je belo, da je izvoz neprozoren. */ 356export async function fetchComposite(bounds, sources, opts = {}) { 357 const b = normalizeBounds(bounds); 358 const { width, height } = pixelSize(b, opts.maxPx ?? 2400, opts.minPx ?? 100); 359 const canvas = document.createElement("canvas"); 360 canvas.width = width; canvas.height = height; 361 const ctx = canvas.getContext("2d"); 362 ctx.fillStyle = "#fff"; 363 ctx.fillRect(0, 0, width, height); 364 // vsi viri se prenesejo VZPOREDNO (GetMap traja tudi po nekaj sekund), 365 // nariÅ¡ejo pa v podanem vrstnem redu (spodnji prvi) 366 const slike = await Promise.all(sources.map(async (s) => { 367 let maxPx = opts.maxPx ?? 2400; 368 if (s.pxCap) maxPx = Math.min(maxPx, s.pxCap); 369 if (s.maxGSD) maxPx = Math.max(Math.min(maxPx, Math.round(longSideMeters(b) / s.maxGSD)), 100); 370 const blob = await fetchScreenshot(bounds, s.layers, { ...opts, url: s.url, arcgis: s.arcgis, maxPx }); 371 return createImageBitmap(blob); 372 })); 373 sources.forEach((s, i) => { 374 ctx.globalAlpha = s.opacity ?? 1; 375 ctx.drawImage(slike[i], 0, 0, width, height); // nižje-loÄljiv vir se raztegne 376 }); 377 ctx.globalAlpha = 1; 378 return new Promise((resolve) => canvas.toBlob(resolve, "image/png")); 379} 380 381/** Za dan seznam cart itemov: pridobi geometrije (parcelaGeom/stavbaGeom), združi v gruÄe 382 (clusterGeoms), in za vsako gruÄo generira SET slik: "kataster" (PARCELE+STAVBE_OBRIS), 383 "bonitete" (PARCELE+BONITETE), "raba" (PARCELE+DEJANSKE_RABE), "namenska" 384 (PARCELE+NAMENSKE_RABE). Vrne [{ bbox, stevilkaGruce, slike: { kataster, bonitete, raba, namenska } }]. */ 385export async function generirajScreenshote(cartItems, opts = {}) { 386 const geoms = []; 387 for (const it of cartItems) { 388 const g = it.tip === "parcela" ? await parcelaGeom(it.ko, it.parcela) : await stavbaGeom(it.ko, it.stavba); 389 if (g) geoms.push(g); 390 else if (opts.onWarning) opts.onWarning(it, "geometrija ni najdena"); 391 } 392 if (!geoms.length) return []; 393 394 const clusters = clusterGeoms(geoms, opts.thresholdKm ?? 5); 395 const out = []; 396 let stevilkaGruce = 0; 397 for (const cluster of clusters) { 398 stevilkaGruce++; 399 const bbox = bboxOf(cluster, opts.padRatio ?? 0.15); 400 // koÅ¡arica ZIP namenoma brez slojev nevarnosti (potres/plazovi) â ti so 401 // izbirni pogledi v orodju Posnetki zemljevida, kjer se zraven doda legenda 402 const imena = ["kataster", "ortofoto", "bonitete", "raba", "namenska"]; 403 const slikeArr = await Promise.all( 404 imena.map((ime) => { 405 const spec = LAYER_SETS[ime]; 406 return typeof spec[0] === "object" 407 ? fetchComposite(bbox, spec, opts) 408 : fetchScreenshot(bbox, spec, opts); 409 }) 410 ); 411 const slike = Object.fromEntries(imena.map((ime, i) => [ime, slikeArr[i]])); 412 out.push({ bbox, stevilkaGruce, slike }); 413 } 414 return out; 415} 416 417/* ââ prilagodljivi posnetki (orodje "Posnetki zemljevida") ââââââââââââââââââââ 418 Skupno risanje obrisa parcele/stavbe na canvas nad WMS EPSG:4326 sliko (linearna 419 preslikava [lng,lat] -> piksel, ker je GetMap v surovih stopinjah). Barva/debelina/halo 420 so parametri â isto funkcijo uporablja tudi "Parcela skozi leta" (dovoljenja.js). */ 421 422/** [lng,lat] -> piksel na platnu za dani bbox {south,west,north,east}. */ 423function vLokalni([lng, lat], b, w, h) { 424 return [((lng - b.west) / (b.east - b.west)) * w, ((b.north - lat) / (b.north - b.south)) * h]; 425} 426 427/** NariÅ¡e obris Polygon/MultiPolygon geometrije na 2D kontekst. Privzeto beli halo + 428 rdeÄ obris (viden na svetlih in temnih posnetkih). barva/debelina/halo prilagodljivi. */ 429export function narisiObrisNaCanvas(ctx, geometry, b, w, h, { barva = "#e02424", debelina = 2.5, halo = true } = {}) { 430 if (!geometry) return; 431 // debelina se skalira z velikostjo platna (referenca 1200 px) â fiksni piksli 432 // so bili na 2400 px slikah vizualno preÅ¡ibki 433 const k = Math.max(1, w / 1200); 434 const risi = (rings) => { 435 for (const ring of rings) { 436 ctx.beginPath(); 437 ring.forEach((c, i) => { const [x, y] = vLokalni(c, b, w, h); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); 438 ctx.closePath(); 439 ctx.stroke(); 440 } 441 }; 442 const risiVse = () => { 443 if (geometry.type === "Polygon") risi(geometry.coordinates); 444 else if (geometry.type === "MultiPolygon") for (const p of geometry.coordinates) risi(p); 445 }; 446 ctx.lineJoin = "round"; 447 if (halo) { ctx.strokeStyle = "rgba(255,255,255,.9)"; ctx.lineWidth = (debelina + 2.5) * k; risiVse(); } 448 ctx.strokeStyle = barva; ctx.lineWidth = debelina * k; risiVse(); 449} 450 451/** Oznaka (npr. "k.o. 2636, 123/4") v vogalu â temna podlaga, bel tekst (kot letnica 452 v dovoljenja). `vrsta` zamakne oznako navzdol za veÄvrstiÄne napise (npr. vrednost 453 potresnega pospeÅ¡ka pod oznako parcele). */ 454export function narisiOznako(ctx, txt, vrsta = 0) { 455 if (!txt) return; 456 const px = Math.max(18, Math.round(ctx.canvas.width / 42)); 457 ctx.font = `600 ${px}px system-ui, sans-serif`; 458 const pad = Math.round(px * 0.35), tw = ctx.measureText(txt).width; 459 const y = 10 + vrsta * (px + pad * 2 + 8); 460 ctx.fillStyle = "rgba(0,0,0,.55)"; 461 ctx.fillRect(10, y, tw + pad * 2, px + pad * 2); 462 ctx.fillStyle = "#fff"; 463 ctx.textBaseline = "top"; 464 ctx.fillText(txt, 10 + pad, y + pad); 465} 466 467/** Parcelne meje iz WFS (GeoJSON features) â tanke temno sive z belim robom, 468 berljive na vsaki podlagi (tematske ploskve, GJI, ortofoto). NadomeÅ¡Äajo 469 zeleni GURS sloj PARCELE na tematskih pogledih. */ 470export function narisiMeje(ctx, features, b, w, h) { 471 for (const f of features || []) { 472 narisiObrisNaCanvas(ctx, f && f.type === "Feature" ? f.geometry : f, b, w, h, 473 { barva: "#333333", debelina: 1.5, halo: true }); 474 } 475} 476
477/** Legenda v spodnji desni kot platna, na beli podlagi, da je berljiva tudi nad 478 ortofotom. `legenda` je en GetLegendGraphic URL (cela slika legende z napisi) 479 ali seznam {url, napis} â takrat se simboli zložijo v vrstice z naÅ¡imi napisi 480 (GJI legende so goli 20x20 simboli). Napaka pri prenosu legende slike ne podre. */ 481async function narisiLegendo(ctx, legenda) { 482 const prenesi = async (url) => { 483 const r = await fetch(url); 484 if (!r.ok || !(r.headers.get("content-type") || "").startsWith("image/")) return null; 485 return createImageBitmap(await r.blob()); 486 }; 487 const { width: w, height: h } = ctx.canvas; 488 const rob = 10; 489 try { 490 if (!Array.isArray(legenda)) { 491 const img = await prenesi(legenda); 492 if (!img) return; 493 // majhne legende poveÄaj na ~30 % platna; velikih (npr. namenska raba, 494 // 623x980) NE pomanjÅ¡uj pod native bolj kot na 45 % viÅ¡ine platna, sicer 495 // besedilo postane neÄitljivo â in nikoli Äez polovico Å¡irine platna 496 const cilj = Math.min(Math.max(h * 0.3, 160), 400); 497 let scale = img.height <= cilj ? cilj / img.height : Math.min(1, (h * 0.45) / img.height); 498 scale = Math.min(scale, (w * 0.5) / img.width); 499 const lw = Math.round(img.width * scale), lh = Math.round(img.height * scale); 500 const pad = Math.round(lh * 0.04) + 4; 501 const x = w - lw - pad * 2 - rob, y = h - lh - pad * 2 - rob; 502 ctx.fillStyle = "rgba(255,255,255,.92)"; 503 ctx.fillRect(x, y, lw + pad * 2, lh + pad * 2); 504 ctx.strokeStyle = "rgba(0,0,0,.25)"; 505 ctx.lineWidth = 1; 506 ctx.strokeRect(x + 0.5, y + 0.5, lw + pad * 2 - 1, lh + pad * 2 - 1); 507 ctx.drawImage(img, x + pad, y + pad, lw, lh); 508 return; 509 } 510 // seznam simbolov z napisi -> vrstice "simbol napis"; vnos je {url, napis} 511 // (prenesena sliÄica, npr. GJI simbol â prenosi teÄejo vzporedno) ali 512 // {barva, napis} (barvni kvadratek, npr. poplavni razredi) 513 const vrstice = (await Promise.all(legenda.map(async (v) => { 514 try { 515 if (v.barva) return { barva: v.barva, napis: v.napis || "" }; 516 const img = await prenesi(v.url); 517 return img ? { img, napis: v.napis || "" } : null; 518 } catch { return null; } 519 }))).filter(Boolean); 520 if (!vrstice.length) return; 521 const fs = Math.max(14, Math.round(w / 85)); 522 ctx.save(); 523 ctx.font = `600 ${fs}px system-ui, sans-serif`; 524 const ikona = Math.round(fs * 1.4), pad = Math.round(fs * 0.6), gap = Math.round(fs * 0.5); 525 const vrsticaH = ikona + Math.round(fs * 0.45); 526 const boxW = Math.max(...vrstice.map((v) => ikona + gap + ctx.measureText(v.napis).width)) + pad * 2; 527 const boxH = vrstice.length * vrsticaH - (vrsticaH - ikona) + pad * 2; 528 const x = w - boxW - rob, y = h - boxH - rob; 529 ctx.fillStyle = "rgba(255,255,255,.92)"; 530 ctx.fillRect(x, y, boxW, boxH); 531 ctx.strokeStyle = "rgba(0,0,0,.25)"; 532 ctx.lineWidth = 1; 533 ctx.strokeRect(x + 0.5, y + 0.5, boxW - 1, boxH - 1); 534 ctx.fillStyle = "#111"; 535 ctx.textBaseline = "middle"; 536 vrstice.forEach((v, i) => { 537 const vy = y + pad + i * vrsticaH; 538 if (v.barva) { 539 ctx.fillStyle = v.barva; 540 ctx.fillRect(x + pad, vy, ikona, ikona); 541 ctx.strokeStyle = "rgba(0,0,0,.35)"; 542 ctx.lineWidth = 1; 543 ctx.strokeRect(x + pad + 0.5, vy + 0.5, ikona - 1, ikona - 1); 544 ctx.fillStyle = "#111"; 545 } else { 546 ctx.drawImage(v.img, x + pad, vy, ikona, ikona); 547 } 548 ctx.fillText(v.napis, x + pad + ikona + gap, vy + ikona / 2); 549 }); 550 ctx.restore(); 551 } catch { /* legenda ni kritiÄna */ } 552} 553 554/* Legenda namenske rabe po meri: serverjeva GetLegendGraphic vsebuje VSE državne 555 kategorije (623x980 px â Äez pol slike), na izrezu pa jih je tipiÄno le nekaj. 556 WFS pove, katere so (PODROBNE_NAMENSKE_RABE_SIFRA/NAZIV_SL), GetStyles (SLD) 557 da preslikavo Å¡ifra -> ime pravila, GetLegendGraphic&rule= pa 20x20 sliÄico 558 posamezne kategorije v uradnem GURS stilu. */ 559 560let nrPravilaCache = null; // Map(Å¡ifra -> ime pravila iz SLD) 561 562async function namenskaPravila() { 563 if (nrPravilaCache) return nrPravilaCache; 564 const r = await fetch(WMS_URL + "?service=WMS&version=1.1.1&request=GetStyles&layers=SI.GURS.KN:NAMENSKE_RABE"); 565 const doc = new DOMParser().parseFromString(await r.text(), "text/xml"); 566 const map = new Map(); 567 for (const rule of doc.getElementsByTagNameNS("*", "Rule")) { 568 const ime = rule.getElementsByTagNameNS("*", "Name")[0]?.textContent?.trim(); 569 if (!ime) continue; 570 for (const lit of rule.getElementsByTagNameNS("*", "Literal")) { 571 const v = lit.textContent?.trim(); 572 if (v && !map.has(v)) map.set(v, ime); 573 } 574 } 575 nrPravilaCache = map; 576 return map; 577} 578
579/** Legenda samo za kategorije namenske rabe, prisotne v izrezu. Vrne seznam 580 {url, napis} za narisiLegendo ali null (-> fallback na celo legendo). */ 581export async function namenskaLegendaZaBbox(bbox) { 582 try { 583 const b = normalizeBounds(bbox); 584 const bounds = window.L.latLngBounds([[b.south, b.west], [b.north, b.east]]); 585 const [fc, pravila] = await Promise.all([namenskeRabeBbox(bounds, 4000), namenskaPravila()]); 586 const vrstice = []; 587 const videni = new Set(); 588 for (const f of fc.features || []) { 589 const p = f.properties || {}; 590 const sifra = String(p.PODROBNE_NAMENSKE_RABE_SIFRA || "").trim(); 591 const rule = pravila.get(sifra); 592 if (!rule || videni.has(rule)) continue; 593 videni.add(rule); 594 const naziv = p.PODROBNE_NAMENSKE_RABE_NAZIV_SL || ""; 595 vrstice.push({ 596 url: WMS_URL + "?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image/png&layer=SI.GURS.KN:NAMENSKE_RABE&rule=" + encodeURIComponent(rule), 597 napis: naziv ? `${sifra} â ${naziv}` : sifra, 598 }); 599 } 600 vrstice.sort((a, b2) => a.napis.localeCompare(b2.napis, "sl")); 601 return vrstice.length ? vrstice.slice(0, 14) : null; // varovalo pred ekstremnimi izrezi 602 } catch { return null; } 603} 604 605/** Oprema karte: merilo (grafiÄna skala) in datum spodaj levo, severnica zgoraj 606 desno â vsak del vklopljiv, videz (svetel/temen) in velikost nastavljiva. 607 o: {merilo=true, severnica=true, datum:string|null, slog:"svetlo"|"temno", 608 velikost:1}. Merilo: razdalja zaokrožena na 1/2/5·10^n m iz realne razdalje 609 na tleh pri srednji zemljepisni Å¡irini (GetMap je v EPSG:4326); sever je pri 610 EPSG:4326 vedno gor. */ 611export function narisiMeriloSeverDatum(ctx, bbox, o = {}) { 612 const { merilo = true, severnica = true, datum = null, vir = null, slog = "svetlo", velikost = 1 } = o; 613 const w = ctx.canvas.width, h = ctx.canvas.height; 614 const temno = slog === "temno"; 615 const bg = temno ? "rgba(22,22,22,.62)" : "rgba(255,255,255,.88)"; 616 const fg = temno ? "#fff" : "#111"; 617 const rob = temno ? "rgba(255,255,255,.3)" : "rgba(0,0,0,.25)"; 618 ctx.save(); 619 if (merilo || datum || vir) { 620 const fs = Math.max(12, Math.round((w / 90) * velikost)); 621 ctx.font = `600 ${fs}px system-ui, sans-serif`; 622 const pad = Math.round(fs * 0.6); 623 // navedba vira: drobna, tanjÅ¡a in zbledela vrstica pod datumom â obvezna 624 // atribucija odprtih podatkov (GURS/ARSO/GeoZS/DRSV), a Äim manj vsiljiva 625 const fsV = Math.max(9, Math.round(fs * 0.72)); 626 let virW = 0; 627 if (vir) { 628 ctx.font = `400 ${fsV}px system-ui, sans-serif`; 629 virW = ctx.measureText(vir).width; 630 ctx.font = `600 ${fs}px system-ui, sans-serif`; 631 } 632 const latMid = (bbox.south + bbox.north) / 2; 633 const mPerPx = ((bbox.east - bbox.west) * KM_PER_DEG_LAT * 1000 * Math.cos(toRad(latMid))) / w; 634 const cilj = mPerPx * w * 0.2; // ~1/5 Å¡irine slike 635 const n = Math.pow(10, Math.floor(Math.log10(cilj))); 636 const dist = [1, 2, 5, 10].map((k) => k * n).reduce((a, b) => (Math.abs(b - cilj) < Math.abs(a - cilj) ? b : a)); 637 const px = dist / mPerPx; 638 const napis = dist >= 1000 ? `${(dist / 1000).toLocaleString("sl-SI")} km` : `${dist} m`; 639 const boxW = Math.max(merilo ? Math.max(px, ctx.measureText(napis).width) : 0, 640 datum ? ctx.measureText(datum).width : 0, 641 virW) + pad * 2; 642 const boxH = (merilo && datum ? fs * 2 + Math.round(fs * 1.1) 643 : merilo ? Math.round(fs * 1.75) 644 : datum ? fs : 0) 645 + pad * 2 646 + (vir ? fsV + ((merilo || datum) ? Math.round(fs * 0.3) : 0) : 0); 647 const x = 10, y = h - boxH - 10; 648 ctx.fillStyle = bg; 649 ctx.fillRect(x, y, boxW, boxH); 650 ctx.strokeStyle = rob; 651 ctx.lineWidth = 1; 652 ctx.strokeRect(x + 0.5, y + 0.5, boxW - 1, boxH - 1); 653 ctx.fillStyle = fg; 654 ctx.textBaseline = "top"; 655 let vy = y + pad - 2; 656 if (merilo) { 657 ctx.fillText(napis, x + pad, vy); 658 const ly = vy + fs + Math.round(fs * 0.42); // Ärta merila s konÄnima zobcema 659 ctx.strokeStyle = fg; 660 ctx.lineWidth = Math.max(2, Math.round(fs / 8)); 661 ctx.beginPath(); 662 ctx.moveTo(x + pad, ly - fs * 0.35); 663 ctx.lineTo(x + pad, ly); 664 ctx.lineTo(x + pad + px, ly); 665 ctx.lineTo(x + pad + px, ly - fs * 0.35); 666 ctx.stroke(); 667 vy = ly + Math.round(fs * 0.35); 668 } 669 if (datum) { ctx.fillText(datum, x + pad, vy); vy += fs + Math.round(fs * 0.3); } 670 if (vir) { 671 ctx.font = `400 ${fsV}px system-ui, sans-serif`; 672 ctx.fillStyle = temno ? "rgba(255,255,255,.72)" : "rgba(0,0,0,.58)"; 673 ctx.fillText(vir, x + pad, vy); 674 } 675 } 676 if (severnica) { 677 const r = Math.max(14, Math.round((w / 55) * velikost));
678 const cx = w - r - 14, cy = r + 14; 679 ctx.fillStyle = bg; 680 ctx.beginPath(); 681 ctx.arc(cx, cy, r, 0, Math.PI * 2); 682 ctx.fill(); 683 ctx.strokeStyle = rob; 684 ctx.lineWidth = 1; 685 ctx.stroke(); 686 ctx.fillStyle = fg; 687 ctx.beginPath(); 688 ctx.moveTo(cx, cy - r * 0.62); 689 ctx.lineTo(cx - r * 0.3, cy + r * 0.05); 690 ctx.lineTo(cx + r * 0.3, cy + r * 0.05); 691 ctx.closePath(); 692 ctx.fill(); 693 ctx.font = `700 ${Math.round(r * 0.55)}px system-ui, sans-serif`; 694 ctx.textAlign = "center"; 695 ctx.textBaseline = "top"; 696 ctx.fillText("S", cx, cy + r * 0.12); 697 } 698 ctx.restore(); 699} 700 701/** Ena slika pogleda za že izraÄunan bbox: prenese WMS sloje (kompozit iz veÄ virov, 702 Äe je `spec` seznam {url,layers}, sicer en vir), po želji nariÅ¡e parcelne meje 703 (mejeGeoms iz WFS), obrise geometrij, oznake, merilo/severnico/datum in legendo, 704 vrne PNG Blob. opts: {maxPx, minPx, url, extraParams, 705 overlayDraw:async (ctx,bbox,w,h)=>{} (npr. poplavni poligoni Äez podlago), 706 mejeGeoms:[Feature], obrisGeoms:[Feature|geom], obris:{barva,debelina,halo}, 707 label | napisi:[string], merilo:bool | oprema:{merilo,severnica,datum:bool, 708 slog,velikost}, legendUrl:string|[{url,napis}|{barva,napis}]}. */ 709export async function slikaPogleda(bbox, spec, opts = {}) { 710 const fetchOpts = { maxPx: opts.maxPx, minPx: opts.minPx, extraParams: opts.extraParams }; 711 const jeKompozit = Array.isArray(spec) && spec.length && typeof spec[0] === "object"; 712 const baseBlob = jeKompozit 713 ? await fetchComposite(bbox, spec, fetchOpts) 714 : await fetchScreenshot(bbox, spec, { ...fetchOpts, url: opts.url }); 715 const imaObris = opts.obrisGeoms && opts.obrisGeoms.length; 716 const img = await createImageBitmap(baseBlob); 717 const canvas = document.createElement("canvas"); 718 canvas.width = img.width; canvas.height = img.height; 719 const ctx = canvas.getContext("2d"); 720 // belo ozadje: goli WMS sloji so prozorni PNG-ji in bi bili v temnih 721 // pregledovalnikih videti Ärni â izvoz je zato vedno neprozoren 722 ctx.fillStyle = "#fff"; 723 ctx.fillRect(0, 0, img.width, img.height); 724 ctx.drawImage(img, 0, 0); 725 if (opts.overlayDraw) await opts.overlayDraw(ctx, normalizeBounds(bbox), img.width, img.height); 726 if (opts.mejeGeoms && opts.mejeGeoms.length) narisiMeje(ctx, opts.mejeGeoms, bbox, img.width, img.height); 727 if (imaObris) for (const g of opts.obrisGeoms) { 728 narisiObrisNaCanvas(ctx, g && g.type === "Feature" ? g.geometry : g, bbox, img.width, img.height, opts.obris || {}); 729 } 730 const napisi = opts.napisi || (opts.label ? [opts.label] : []); 731 napisi.forEach((n, i) => narisiOznako(ctx, n, i)); 732 const oprema = opts.oprema ?? (opts.merilo ? {} : null); 733 if (oprema) { 734 const oo = { 735 merilo: oprema.merilo ?? true, 736 severnica: oprema.severnica ?? true, 737 datum: (oprema.datum ?? true) ? new Date().toLocaleDateString("sl-SI") + (izvozRacunEmail() ? " · " + izvozRacunEmail() : "") : null, 738 vir: oprema.vir === false ? null : (oprema.vir || "Vir: GURS"), 739 slog: oprema.slog || "svetlo", 740 velikost: oprema.velikost || 1, 741 }; 742 if (oo.merilo || oo.severnica || oo.datum || oo.vir) narisiMeriloSeverDatum(ctx, normalizeBounds(bbox), oo); 743 } 744 if (opts.legendUrl) await narisiLegendo(ctx, opts.legendUrl); 745 return new Promise((res) => canvas.toBlob(res, "image/png")); 746}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.