sagadeck 1.7.1 → 1.8.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.
Files changed (46) hide show
  1. package/README.md +40 -0
  2. package/docs/ANDAMENTO.md +122 -0
  3. package/docs/REFERENCIA.md +58 -0
  4. package/package.json +2 -1
  5. package/src/ai/deck-ai.js +13 -4
  6. package/src/ai/map-analysis-run.js +119 -0
  7. package/src/ai/map-data.js +221 -0
  8. package/src/build.js +6 -2
  9. package/src/export/browser.js +23 -1
  10. package/src/export/pptx.js +2 -1
  11. package/src/export/shots.js +3 -1
  12. package/src/layouts.js +10 -0
  13. package/src/map-analysis.js +106 -0
  14. package/src/map-config.js +83 -0
  15. package/src/map-routing.js +82 -0
  16. package/src/map-services.js +60 -0
  17. package/src/map.js +261 -0
  18. package/src/runtime/base.css +20 -0
  19. package/src/runtime/map.js +234 -0
  20. package/src/runtime/runtime.js +1 -1
  21. package/src/runtime/vendor/LEAFLET-LICENSE.txt +26 -0
  22. package/src/runtime/vendor/geo-contornos.json +1 -0
  23. package/src/runtime/vendor/leaflet.css +661 -0
  24. package/src/runtime/vendor/leaflet.js +6 -0
  25. package/src/studio/ai-chat-route.js +20 -0
  26. package/src/studio/demo-decks.js +3 -1
  27. package/src/studio/geo-routes.js +138 -0
  28. package/src/studio/layout-samples.js +71 -0
  29. package/src/studio/project.js +1 -1
  30. package/src/studio/public/ai-settings.js +4 -1
  31. package/src/studio/public/app.js +7 -3
  32. package/src/studio/public/explorer.js +23 -8
  33. package/src/studio/public/index.html +4 -0
  34. package/src/studio/public/library.html +2 -0
  35. package/src/studio/public/library.js +6 -1
  36. package/src/studio/public/map-data.js +154 -0
  37. package/src/studio/public/map-editor.js +287 -0
  38. package/src/studio/public/map-fields.js +21 -0
  39. package/src/studio/public/map-settings.js +84 -0
  40. package/src/studio/public/slide-form.js +1 -1
  41. package/src/studio/public/style.css +13 -0
  42. package/src/studio/public/ui-icons.js +1 -1
  43. package/src/studio/public/viewers.js +2 -1
  44. package/src/studio/public/visual-editor.js +1 -1
  45. package/src/studio/server.js +10 -0
  46. package/src/studio/tour.js +76 -0
@@ -0,0 +1,234 @@
1
+ // Mapa ao vivo do slide `map` (dados montados por src/map.js). Regras:
2
+ // - preguiçoso: só monta quando o slide aparece (na apresentação) ou quando o Studio pede (prévia grande); o Leaflet
3
+ // só carrega na primeira vez (no HTML exportado ele já vem embutido);
4
+ // - uma tentativa e para: se os primeiros tiles falham sem nenhum sucesso, o fundo sai (as camadas ficam), aparece o
5
+ // aviso, e nenhum outro mapa desta sessão tenta de novo até alguém clicar em "Tentar de novo";
6
+ // - nada de baixar tiles fora da tela; atribuição sempre visível.
7
+ (function () {
8
+ const FALHAS = 6; // tiles que podem falhar, sem nenhum sucesso, antes de desligar o fundo
9
+ const KEY = "sagadeck.mapa.fundo";
10
+ const store = { get() { try { return sessionStorage.getItem(KEY); } catch { return null; } }, set(v) { try { v ? sessionStorage.setItem(KEY, v) : sessionStorage.removeItem(KEY); } catch { /* sem armazenamento: só nesta página */ } } };
11
+ let broken = store.get() === "falhou";
12
+ let loader;
13
+
14
+ function load() {
15
+ if (window.L) return Promise.resolve();
16
+ return loader ||= new Promise((resolve, reject) => {
17
+ if (!document.querySelector('link[data-leaflet]')) { const css = document.createElement("link"); css.rel = "stylesheet"; css.href = "leaflet.css"; css.dataset.leaflet = "1"; document.head.append(css); }
18
+ const s = document.createElement("script"); s.src = "leaflet.js"; s.onload = resolve;
19
+ s.onerror = () => { loader = null; s.remove(); reject(new Error("Não foi possível carregar o mapa.")); };
20
+ document.head.append(s);
21
+ });
22
+ }
23
+
24
+ // cor do tema (s1…, em, #hex) em rgb, lida do próprio slide
25
+ function resolver(box) {
26
+ const probe = document.createElement("i"); probe.style.display = "none"; box.append(probe);
27
+ const cache = {};
28
+ const rgb = (c) => {
29
+ if (cache[c]) return cache[c];
30
+ // a mesma regra de src/map.js (mapColorCSS): paleta do tema c1…c5 com recuo no destaque
31
+ probe.style.color = /^#?[0-9a-f]{6}$/i.test(c) ? `#${c.replace("#", "")}` : /^(c\d|accent|alert|ink|paper)$/.test(c) ? `var(--c-${c},var(--em))` : `var(--${c})`;
32
+ const m = getComputedStyle(probe).color.match(/\d+(\.\d+)?/g) || [0, 0, 0];
33
+ return (cache[c] = m.slice(0, 3).map(Number));
34
+ };
35
+ const css = (c) => `rgb(${rgb(c).join(",")})`;
36
+ const mix = (a, b, t) => `rgb(${rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * t)).join(",")})`;
37
+ return { css, mix, done: () => probe.remove() };
38
+ }
39
+ const num = (v) => { const s = String(v ?? "").trim().replace(/^R\$/i, "").replace(/%$/, "").replace(/\s/g, ""); const n = Number(/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(s) ? s.replace(/\./g, "").replace(",", ".") : s.replace(",", ".")); return s !== "" && Number.isFinite(n) ? n : null; };
40
+ const fmt = (n) => (Number.isFinite(n) ? n.toLocaleString(document.documentElement.lang || "pt-BR", { maximumFractionDigits: 1 }) : "");
41
+ const escHTML = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
42
+ // exportando (PDF, PowerPoint, fotos) também sem animação: a foto sai no lugar final
43
+ const reduced = () => matchMedia("(prefers-reduced-motion: reduce)").matches || document.documentElement.dataset.motion === "none" || document.documentElement.classList.contains("export");
44
+
45
+ const live = new Map(); // .map-box -> estado
46
+ // vista guardada entre redesenhos do Studio (chave: o slide e a sua forma de enquadrar)
47
+ const views = new Map();
48
+ const keyOf = (box) => { const s = box.closest("[data-idx]"); return s && !s.closest("#stage") ? `${s.dataset.uid || s.dataset.idx}|${box.querySelector(".map-data")?.textContent.match(/"view":(null|\{[^}]*\}|"fit")/)?.[1] || ""}` : null; };
49
+ let readyResolve, readyPromise = Promise.resolve();
50
+
51
+ function notice(box, text, retry) {
52
+ let n = box.querySelector(".map-notice");
53
+ if (!n) { n = document.createElement("div"); n.className = "map-notice f-label"; box.append(n); }
54
+ n.innerHTML = `<span>${escHTML(text)}</span>${retry ? '<button type="button">Tentar de novo</button>' : ""}`;
55
+ n.querySelector("button")?.addEventListener("click", (e) => { e.stopPropagation(); store.set(null); broken = false; n.remove(); const st = live.get(box); if (st) { dispose(box); mount(box.closest(".slide") || box.parentElement, { force: true }); } });
56
+ }
57
+
58
+ async function mountBox(box) {
59
+ if (live.has(box) || box.dataset.mapMounted) return;
60
+ box.dataset.mapMounted = "pending";
61
+ let m;
62
+ try { m = JSON.parse(box.querySelector(".map-data").textContent); } catch { box.dataset.mapMounted = "error"; return; }
63
+ let settle; const settled = new Promise((r) => (settle = r));
64
+ const st = { m, settled, map: null, layers: [], tilesDone: false };
65
+ live.set(box, st);
66
+ try { await load(); } catch (e) { notice(box, e.message); box.dataset.mapMounted = "error"; settle(); return; }
67
+ const L = window.L;
68
+ const el = box.querySelector(".map-live");
69
+ const map = L.map(el, { zoomControl: true, attributionControl: true, keyboard: false, worldCopyJump: true, zoomSnap: 0.25, fadeAnimation: !reduced(), zoomAnimation: !reduced() });
70
+ st.map = map;
71
+ map.attributionControl.setPrefix('<a href="https://leafletjs.com" target="_blank" rel="noopener">Leaflet</a>');
72
+ // a roda e o arrastar mexem no mapa, não trocam de slide
73
+ for (const ev of ["wheel", "pointerdown", "mousedown", "touchstart", "click", "keydown"]) el.addEventListener(ev, (e) => e.stopPropagation());
74
+
75
+ // fundo: antes de ligar a camada, testa UM tile (o do centro). Se ele falha, nenhum outro é pedido e nenhum
76
+ // outro mapa desta sessão tenta; se passa, a camada liga (e ainda desiste se os primeiros falharem todos)
77
+ const wantSat = m.basemap === "satelite";
78
+ const tiles = m.basemap === "nenhum" ? null : (wantSat && m.tiles?.satelite) || m.tiles?.tiles || null;
79
+ if (wantSat && !m.tiles?.satelite) notice(box, "Satélite não configurado nesta máquina (Configurar mapa); mostrando as ruas.");
80
+ const giveUp = () => {
81
+ broken = true; store.set("falhou"); box.dataset.mapTiles = "falhou";
82
+ notice(box, "Mapa de fundo indisponível nesta rede. As camadas continuam.", true);
83
+ st.tilesDone = true; settle();
84
+ };
85
+ const startTiles = () => {
86
+ if (!tiles) { st.tilesDone = true; return; }
87
+ if (broken) { box.dataset.mapTiles = "falhou"; notice(box, "Mapa de fundo indisponível nesta rede. As camadas continuam.", true); st.tilesDone = true; return; }
88
+ const z = Math.round(map.getZoom()), c = map.project(map.getCenter(), z).divideBy(256).floor();
89
+ const probe = new Image();
90
+ probe.onerror = () => { if (live.get(box) === st) giveUp(); };
91
+ probe.onload = () => {
92
+ if (live.get(box) !== st) return;
93
+ let ok = 0, fail = 0;
94
+ const t = L.tileLayer(tiles.url, { attribution: tiles.attribution, maxZoom: tiles.maxZoom || 19 });
95
+ t.on("tileload", () => { ok++; });
96
+ t.on("tileerror", () => { fail++; if (!ok && fail >= FALHAS) { map.removeLayer(t); giveUp(); } });
97
+ t.on("load", () => { st.tilesDone = true; settle(); });
98
+ t.addTo(map);
99
+ };
100
+ probe.src = L.Util.template(tiles.url, { z, x: c.x, y: c.y, s: "a", r: "" });
101
+ };
102
+
103
+ // camadas
104
+ const col = resolver(box);
105
+ const legend = document.createElement("div"); legend.className = "map-legend f-body";
106
+ m.layers.forEach((layer, i) => {
107
+ const s = layer.style;
108
+ const hidden = new Set();
109
+ const colorOf = (f) => {
110
+ const v = s.colorBy ? f.properties?.[s.colorBy] : null;
111
+ if (s.scale) { const n = num(v); const t = n == null || s.scale.max === s.scale.min ? 1 : (n - s.scale.min) / (s.scale.max - s.scale.min); return col.mix("surface", "em", 0.2 + 0.8 * t); }
112
+ const cat = s.categories?.find((c) => c.value === String(v ?? "").trim());
113
+ return col.css(cat?.color || s.color || "c1");
114
+ };
115
+ const radius = (f) => { if (!s.sizeBy) return 14; const n = num(f.properties?.[s.sizeBy]); const r = s.sizeScale; return n == null || r.max === r.min ? 16 : 9 + 24 * Math.sqrt((n - r.min) / (r.max - r.min)); }; // em px do slide (1920)
116
+ const card = (f) => {
117
+ const p = f.properties || {};
118
+ const title = s.label ? p[s.label] : p.nome || p.name || "";
119
+ const rows = s.popup.filter((c) => c !== s.label).map((c) => `<tr><th>${escHTML(c)}</th><td>${escHTML(p[c])}</td></tr>`).join("");
120
+ return `<div class="map-card-in">${title ? `<b>${escHTML(title)}</b>` : ""}${rows ? `<table>${rows}</table>` : ""}</div>`;
121
+ };
122
+ const g = L.geoJSON(layer.data, {
123
+ filter: (f) => !s.colorBy || !hidden.has(String(f.properties?.[s.colorBy] ?? "").trim()),
124
+ pointToLayer: (f, ll) => L.circleMarker(ll, { radius: radius(f), color: col.css("bg"), weight: 2.5, fillColor: colorOf(f), fillOpacity: 0.92 }),
125
+ style: (f) => (f.geometry.type.includes("Polygon") ? { color: colorOf(f), weight: 2, fillColor: colorOf(f), fillOpacity: layer.kind === "areas" ? 0.75 : 0.32 } : { color: colorOf(f), weight: 5, opacity: 0.95 }),
126
+ onEachFeature: (f, lyr) => {
127
+ if (s.popup.length || s.label) lyr.bindPopup(card(f), { className: "map-card", maxWidth: 420 });
128
+ // no Studio, clicar no ponto seleciona a linha da planilha aberta (map-data.js)
129
+ lyr.on("click", () => document.dispatchEvent(new CustomEvent("sagamap:ponto", { detail: { path: layer.source, linha: f.properties?._linha } })));
130
+ if (s.label && f.properties?.[s.label] != null && layer.data.features.length <= 60) lyr.bindTooltip(escHTML(f.properties[s.label]), { permanent: true, direction: "top", offset: [0, -10], className: "map-label" });
131
+ },
132
+ });
133
+ const entry = { layer, g, on: layer.step == null, hidden };
134
+ st.layers.push(entry);
135
+ if (entry.on) g.addTo(map);
136
+ // legenda: o nome liga/desliga a camada; cada categoria liga/desliga os seus pontos
137
+ const item = document.createElement("div"); item.className = "map-leg-layer"; item.dataset.layer = i;
138
+ const cats = s.categories ? s.categories.map((c) => `<button type="button" class="map-leg-cat" data-cat="${escHTML(c.value)}"><i style="background:${col.css(c.color)}"></i>${escHTML(c.value)}</button>`).join("") : "";
139
+ const scale = s.scale ? `<div class="map-leg-scale"><span>${escHTML(layer.prefix + fmt(s.scale.min) + layer.suffix)}</span><i style="background:linear-gradient(90deg,${col.mix("surface", "em", 0.2)},${col.css("em")})"></i><span>${escHTML(layer.prefix + fmt(s.scale.max) + layer.suffix)}</span></div>` : "";
140
+ item.innerHTML = `<button type="button" class="map-leg-name">${!s.categories && !s.scale ? `<i style="background:${col.css(s.color || "c1")}"></i>` : ""}${escHTML(layer.name)}${layer.legend ? ` <small>${escHTML(layer.legend)}</small>` : ""}</button>${cats}${scale}`;
141
+ item.querySelector(".map-leg-name").onclick = (e) => { e.stopPropagation(); entry.manualOff = !entry.manualOff; sync(entry); item.classList.toggle("off", entry.manualOff); };
142
+ item.querySelectorAll(".map-leg-cat").forEach((b) => (b.onclick = (e) => {
143
+ e.stopPropagation();
144
+ const v = b.dataset.cat; hidden.has(v) ? hidden.delete(v) : hidden.add(v); b.classList.toggle("off", hidden.has(v));
145
+ g.clearLayers(); g.addData(layer.data);
146
+ }));
147
+ item.hidden = !entry.on;
148
+ entry.legend = item;
149
+ legend.append(item);
150
+ });
151
+ col.done();
152
+ if (m.layers.length) box.append(legend);
153
+ const sync = (entry) => {
154
+ const show = entry.on && !entry.manualOff;
155
+ if (show && !map.hasLayer(entry.g)) entry.g.addTo(map); else if (!show && map.hasLayer(entry.g)) map.removeLayer(entry.g);
156
+ if (entry.legend) entry.legend.hidden = !entry.on;
157
+ };
158
+
159
+ // enquadramento: o do slide, senão os dados, senão o mundo
160
+ // O enquadramento automático deixa os pontos acima da legenda, para continuarem visíveis e clicáveis.
161
+ const fit = (b, fly) => { if (!b) return; const opts = { paddingTopLeft: [48, 48], paddingBottomRight: [48, 48 + legend.offsetHeight], maxZoom: 17 }; fly && !reduced() ? map.flyToBounds(b, { ...opts, duration: 1.6 }) : map.fitBounds(b, opts); };
162
+ const go = (v, fly) => { if (!v) return; if (v === "fit") return; fly && !reduced() ? map.flyTo([v.center[0], v.center[1]], v.zoom, { duration: 1.6 }) : map.setView([v.center[0], v.center[1]], v.zoom); };
163
+ // no Studio, o slide redesenha a cada edição: a vista que a pessoa estava olhando continua (na apresentação, não)
164
+ const kept = keyOf(box) && views.get(keyOf(box));
165
+ if (kept) map.setView(kept.center, kept.zoom, { animate: false });
166
+ else if (m.view && m.view !== "fit") go(m.view); else if (m.bounds) fit(m.bounds); else map.setView([0, 0], 2);
167
+ startTiles();
168
+
169
+ // camada por clique: os marcadores invisíveis data-step do slide ganham "in" (src/runtime/runtime.js)
170
+ const steps = [...box.querySelectorAll(".map-step")];
171
+ const apply = (fly) => {
172
+ let lastView = null;
173
+ for (const s of steps) {
174
+ const entry = st.layers[Number(s.dataset.layer)]; if (!entry) continue;
175
+ const on = s.classList.contains("in");
176
+ if (on !== entry.on) { entry.on = on; sync(entry); if (on && entry.layer.view) lastView = entry; }
177
+ }
178
+ if (lastView && fly) lastView.layer.view === "fit" ? fit(lastView.g.getBounds(), true) : go(lastView.layer.view, true);
179
+ };
180
+ apply(false);
181
+ st.observer = new MutationObserver(() => apply(true));
182
+ steps.forEach((s) => st.observer.observe(s, { attributes: true, attributeFilter: ["class"] }));
183
+
184
+ box.dataset.mapMounted = "ready";
185
+ requestAnimationFrame(() => map.invalidateSize());
186
+ if (st.tilesDone) settle();
187
+ setTimeout(settle, 8000); // a exportação não espera para sempre
188
+ }
189
+
190
+ // linha selecionada na planilha (Studio) -> abre o cartão do ponto dessa linha
191
+ document.addEventListener("sagamap:linha", (e) => {
192
+ const { path, linha } = e.detail || {};
193
+ for (const st of live.values()) for (const entry of st.layers) {
194
+ if (!path || entry.layer.source !== path || !st.map?.hasLayer(entry.g)) continue;
195
+ entry.g.eachLayer((lyr) => { if (lyr.feature?.properties?._linha === linha) { lyr.openPopup(); if (lyr.getLatLng) st.map.panTo(lyr.getLatLng()); } });
196
+ }
197
+ });
198
+
199
+ function mount(root = document, { force = false } = {}) {
200
+ const boxes = [...root.querySelectorAll(".map-box")];
201
+ if (root.classList?.contains("map-box")) boxes.push(root);
202
+ const jobs = boxes.filter((b) => force || !live.has(b)).map(mountBox);
203
+ readyPromise = Promise.all(jobs).then(() => Promise.all(boxes.map((b) => live.get(b)?.settled)));
204
+ return readyPromise;
205
+ }
206
+ function dispose(root = document, { keepView = false } = {}) {
207
+ const boxes = root.classList?.contains("map-box") ? [root] : [...root.querySelectorAll(".map-box")];
208
+ for (const b of boxes) {
209
+ const st = live.get(b); if (!st) continue;
210
+ if (keepView && keyOf(b) && st.map) views.set(keyOf(b), { center: st.map.getCenter(), zoom: st.map.getZoom() });
211
+ st.observer?.disconnect(); st.map?.remove(); live.delete(b);
212
+ delete b.dataset.mapMounted; b.querySelector(".map-legend")?.remove();
213
+ b.querySelector(".map-notice")?.remove();
214
+ }
215
+ }
216
+
217
+ // na apresentação: só o slide atual monta (o resto não pede tile nenhum); a exportação espera o "pronto"
218
+ function watchDeck() {
219
+ const slides = [...document.querySelectorAll("#stage > .slide")].filter((s) => s.querySelector(".map-box"));
220
+ if (!slides.length) return;
221
+ const check = () => { for (const s of slides) if (s.classList.contains("current")) mount(s); };
222
+ const obs = new MutationObserver(check);
223
+ slides.forEach((s) => obs.observe(s, { attributes: true, attributeFilter: ["class"] }));
224
+ check();
225
+ }
226
+ window.SagaMap = {
227
+ mount, dispose,
228
+ // o editor do Studio (map-editor.js) pega o mapa e as camadas montadas desta caixa
229
+ instance: (box) => live.get(box),
230
+ // a exportação chama depois de ir a um slide: espera o mapa dele (tiles ou desistência), no máximo 8 s
231
+ settled: () => { const cur = document.querySelector("#stage > .slide.current"); return cur ? mount(cur) : Promise.resolve(); },
232
+ };
233
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", watchDeck); else watchDeck();
234
+ })();
@@ -819,7 +819,7 @@
819
819
  const t = e.changedTouches[0], dx = t.clientX - tx, dy = t.clientY - ty, dt = Date.now() - tt;
820
820
  tx = ty = null;
821
821
  if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) { cmd(dx < 0 ? "next" : "prev"); return; }
822
- if (Math.abs(dx) < 12 && Math.abs(dy) < 12 && dt < 500 && !e.target.closest("a,button,input,textarea,select,[contenteditable],video,audio,iframe,.widget,[data-goto]")) {
822
+ if (Math.abs(dx) < 12 && Math.abs(dy) < 12 && dt < 500 && !e.target.closest("a,button,input,textarea,select,[contenteditable],video,audio,iframe,.widget,.map-box,[data-goto]")) {
823
823
  const sel = typeof getSelection === "function" && getSelection();
824
824
  if (sel && !sel.isCollapsed) return; // selecionando texto: não avança
825
825
  cmd("next");
@@ -0,0 +1,26 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2010-2023, Volodymyr Agafonkin
4
+ Copyright (c) 2010-2011, CloudMade
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.