PageSourceSearch

https://parcelnik.si/js/screenshot.js?v=281

js parcelnik.si collected 2026-09-24 15:11:45 UTC 38,357 bytes, 746 lines download raw bytes

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.