/** * Optech GIS PWA — אפליקציית שטח להצגת שרטוטי DXF על מפות רקע. * * שני מצבי תצוגה: * - leaflet: מפות רקע ציבוריות (OSM/Esri/Google) + שכבות וקטוריות. * - govmap: הווידג'ט הרשמי של פורטל המפות הממשלתי (דורש טוקן API * רשום לדומיין); השרטוטים נדחפים אליו כ-WKT ברשת ישראל. */ (function () { "use strict"; var DEFAULT_CENTER = [31.778, 35.235]; // ירושלים var DEFAULT_ZOOM = 15; var BASE_PREF_KEY = "gis.basemap"; // 'osm' | ... | 'govmap:1' var state = { mode: "leaflet", // 'leaflet' | 'govmap' map: null, baseKey: localStorage.getItem(BASE_PREF_KEY) || "osm", baseLayer: null, files: [], // רשומות מ-IndexedDB overlays: {}, // id → {group, byLayer, textMarkers, transform} labelsOn: true, loc: { watchId: null, marker: null, circle: null, follow: false }, cal: null, // {fileId, pairs:[{src,dstItm}], pins:[], step} draw: { mode: null, color: "#e53935", temp: [], tempLatLngs: [], tempLayer: null }, assign: { mode: null, sel: null, selMarker: null }, // מצב שיוך נכסים searchMarker: null, }; // ---------- עזרי UI ---------- function $(id) { return document.getElementById(id); } function toast(msg, ms) { var el = $("toast"); el.textContent = msg; el.classList.add("show"); clearTimeout(el._t); el._t = setTimeout(function () { el.classList.remove("show"); }, ms || 2600); } function openPanel(id) { ["panel-layers", "panel-basemap", "panel-settings"].forEach(function (p) { $(p).classList.toggle("open", p === id); }); } function closePanels() { openPanel(null); } function loading(on, text) { $("loading-overlay").classList.toggle("show", !!on); if (text) $("loading-text").textContent = text; } function inGovmap() { return state.mode === "govmap"; } // ---------- מפת Leaflet ---------- function buildMap(center, zoom) { var map = L.map("map", { zoomControl: false, // רינדור Canvas — קריטי לשכבות כבדות (עשרות אלפי קווים): // אלמנט ציור אחד במקום עשרות אלפי אלמנטי SVG preferCanvas: true, // leaflet-rotate: סיבוב בשתי אצבעות (או Shift+גרירה בדסקטופ); // כפתור המצפן מיישר חזרה לצפון rotate: true, touchRotate: true, shiftKeyRotate: true, rotateControl: { closeOnZeroBearing: false, position: "bottomright" }, }).setView(center, zoom); L.control.zoom({ position: "bottomright" }).addTo(map); L.control.scale({ imperial: false, position: "bottomleft" }).addTo(map); state.map = map; window.__gisMap = map; // לבדיקות אוטומטיות map.on("moveend zoomend", refreshVisibleLabels); map.on("click", onLeafletClick); refreshVisibleLabels(); return map; } function setLeafletBase(key) { var def = Basemaps.list[key]; if (!def) { key = "osm"; def = Basemaps.list.osm; } if (inGovmap()) exitGovmapMode(); if (state.baseLayer) state.map.removeLayer(state.baseLayer); state.baseLayer = def.make().addTo(state.map); state.baseKey = key; localStorage.setItem(BASE_PREF_KEY, key); $("basemap-name").textContent = def.name.split("—")[0].trim(); document.body.classList.toggle("dark-base", !!def.dark); // צבעי ACI 7 (שחור/לבן) תלויים ברקע — רינדור מחדש state.files.forEach(function (f) { attachOverlay(f); }); renderBasemapPanel(); } var LABEL_CAP = 800; // מקסימום תוויות DOM בו-זמנית (לכל הקבצים יחד) /** * יוצר אלמנטי תווית רק לטקסטים שבתוך אזור התצוגה, עד תקרה. * כך גם מפת כתובות עם 100 אלף טקסטים לא מכבידה — בכל רגע יש * במסך לכל היותר כמה מאות אלמנטים. */ function refreshVisibleLabels() { if (!state.map) return; var zoomOk = state.map.getZoom() >= Render.TEXT_MIN_ZOOM; $("map").classList.toggle("show-labels", !!(state.labelsOn && zoomOk)); var bounds = state.map.getBounds().pad(0.2); var budget = LABEL_CAP; state.files.forEach(function (file) { var ov = state.overlays[file.id]; if (!ov || !ov.texts) return; if (!ov.textGroup) ov.textGroup = L.layerGroup(); var want = file.visible !== false && state.labelsOn && (file.isNotes || zoomOk); ov.textGroup.clearLayers(); if (!want || !ov.texts.length) { if (state.map.hasLayer(ov.textGroup)) state.map.removeLayer(ov.textGroup); return; } if (!state.map.hasLayer(ov.textGroup)) ov.textGroup.addTo(state.map); var hidden = file.hiddenLayers || []; for (var i = 0; i < ov.texts.length && budget > 0; i++) { var t = ov.texts[i]; if (hidden.indexOf(t.layer) !== -1) continue; if (!file.isNotes && !bounds.contains(t.ll)) continue; ov.textGroup.addLayer(L.marker(t.ll, { interactive: false, keyboard: false, icon: L.divIcon({ className: "dxf-text" + (file.isNotes ? " dxf-text-always" : ""), html: t.html, iconSize: null, }), })); budget--; } }); } // ---------- מצב govmap ---------- function enterGovmapMode(bgId) { if (!GovMap.hasToken()) { loadSettingsForm(); openPanel("panel-settings"); toast("להצגת govmap צריך טוקן API — מלאו אותו כאן (הסבר בפאנל)", 4500); return; } if (!navigator.onLine) { toast("מפת govmap דורשת חיבור לאינטרנט"); return; } loading(true, "טוען את מפת govmap…"); GovMap.loadApi().then(function () { var c = state.map.getCenter(); var centerItm = null; try { centerItm = Geo.wgsToItm([c.lat, c.lng]); } catch (e) { /* ברירת מחדל של govmap */ } var level = GovMap.levelForLeafletZoom(state.map.getZoom()); return GovMap.ensureMap("govmap", centerItm, level).then(function () { // אם המפה כבר הייתה קיימת — התמקדות למיקום הנוכחי GovMap.zoomTo(centerItm || [219529, 626907], level, false); }); }).then(function () { GovMap.setBackground(bgId); state.mode = "govmap"; $("map").style.display = "none"; $("govmap").style.display = "block"; $("basemap-name").textContent = "govmap"; document.body.classList.toggle("dark-base", bgId === "1" || bgId === "2"); localStorage.setItem(BASE_PREF_KEY, "govmap:" + bgId); renderBasemapPanel(); return pushAllToGovmap(); }).then(function () { loading(false); }).catch(function (err) { loading(false); state.mode = "leaflet"; $("govmap").style.display = "none"; $("map").style.display = "block"; if (String(err && err.message).indexOf("GOVMAP_API_LOAD") !== -1) { toast("לא ניתן לטעון את govmap — בדקו חיבור לאינטרנט", 4500); } else { toast("מפת govmap לא נטענה — ודאו שהטוקן תקף ורשום לדומיין הזה", 6000); } }); } function exitGovmapMode() { if (!inGovmap()) return; state.mode = "leaflet"; $("govmap").style.display = "none"; $("map").style.display = "block"; state.map.invalidateSize(); // סנכרון התצוגה לפי המיקום האחרון במפת govmap var ext = GovMap.lastExtent(); if (ext && isFinite(ext.xmin)) { try { var sw = Geo.itmToWgs([ext.xmin, ext.ymin]); var ne = Geo.itmToWgs([ext.xmax, ext.ymax]); state.map.fitBounds(L.latLngBounds([sw, ne])); } catch (e) { /* נשארים בתצוגה הקודמת */ } } if (GovMap.isGpsOn()) GovMap.toggleGps(); } function pushAllToGovmap() { var chain = Promise.resolve(); state.files.forEach(function (f) { chain = chain.then(function () { return syncFileToGovmap(f); }); }); return chain; } function syncFileToGovmap(file) { if (!inGovmap() || !GovMap.isCreated()) return Promise.resolve(); if (file.visible === false) { GovMap.clearFile(file.id); return Promise.resolve(); } var toItm = Geo.buildItmTransform(file, file._anchorItm || anchorItm()); var dark = document.body.classList.contains("dark-base"); return GovMap.pushFile(file, toItm, { showTexts: state.labelsOn, darkBase: dark }) .then(function (r) { if (r && r.truncated) toast("‏" + file.name + ": הקובץ גדול — חלק מהישויות לא הוצגו על govmap", 4500); }); } // ---------- שכבות DXF (Leaflet) ---------- function isDark() { var def = Basemaps.list[state.baseKey]; return !!(def && def.dark); } function anchorItm() { if (!state.map) return null; var c = state.map.getCenter(); try { return Geo.wgsToItm([c.lat, c.lng]); } catch (e) { return null; } } function attachOverlay(file) { detachOverlay(file.id); if (file.visible === false) { renderFilesPanel(); return; } var transform = Geo.buildTransform(file, file._anchorItm || anchorItm()); if (file.coordSys === "unplaced" && !file._anchorItm) file._anchorItm = anchorItm(); var built = Render.buildLayers(file, transform, isDark()); (file.hiddenLayers || []).forEach(function (name) { if (built.byLayer[name]) built.group.removeLayer(built.byLayer[name]); }); built.group.addTo(state.map); built.transform = transform; state.overlays[file.id] = built; refreshVisibleLabels(); renderFilesPanel(); } function detachOverlay(id) { var ov = state.overlays[id]; if (ov && state.map) { state.map.removeLayer(ov.group); if (ov.textGroup && state.map.hasLayer(ov.textGroup)) state.map.removeLayer(ov.textGroup); } delete state.overlays[id]; } function zoomToFile(file) { if (inGovmap()) { var toItm = Geo.buildItmTransform(file, file._anchorItm || anchorItm()); var b = file.bbox; var c = toItm([(b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2]); // רמת זום לפי רוחב הקובץ: היוריסטיקה פשוטה var span = Math.max(1, b.maxX - b.minX, b.maxY - b.minY); var level = Math.max(0, Math.min(10, Math.round(10 - Math.log(span / 150) / Math.LN2))); GovMap.zoomTo(c, level, false); return; } var ov = state.overlays[file.id]; var transform = ov ? ov.transform : Geo.buildTransform(file, anchorItm()); try { state.map.fitBounds(Render.fileBounds(file, transform), { padding: [30, 30] }); } catch (e) { toast("לא ניתן להתמקד בקובץ"); } } // ---------- חיפוש כתובת ---------- function openSearch() { $("search-overlay").classList.add("open"); $("search-input").value = ""; $("search-results").innerHTML = ""; setTimeout(function () { $("search-input").focus(); }, 100); } function closeSearch() { $("search-overlay").classList.remove("open"); } function runSearch() { var q = $("search-input").value.trim(); if (q.length < 2) return; var host = $("search-results"); host.innerHTML = '
מחפש…
'; Search.query(q).then(function (results) { if (!results.length) { host.innerHTML = '
לא נמצאו תוצאות
'; return; } host.innerHTML = ""; results.forEach(function (r) { var div = document.createElement("div"); div.className = "search-result"; div.textContent = r.name; div.addEventListener("click", function () { focusSearchResult(r); }); host.appendChild(div); }); }).catch(function () { host.innerHTML = '
שגיאת חיפוש — בדקו חיבור לאינטרנט
'; }); } function focusSearchResult(r) { closeSearch(); if (inGovmap()) { try { var itm = Geo.wgsToItm([r.lat, r.lng]); GovMap.zoomTo(itm, 9, true); } catch (e) { toast("שגיאת המרה"); } return; } if (state.searchMarker) state.map.removeLayer(state.searchMarker); state.searchMarker = L.marker([r.lat, r.lng]).addTo(state.map) .bindPopup(escapeHtml(r.name.split(",")[0])).openPopup(); state.map.setView([r.lat, r.lng], 17); } // ---------- שכבת הערות: ציור ---------- function toggleNotesToolbar() { if (inGovmap()) { toast("עריכת הערות זמינה על רקעי OSM/לוויין — ההערות עצמן מוצגות גם ב-govmap", 4500); return; } var open = !$("notes-toolbar").classList.contains("open"); if (open && $("assign-toolbar").classList.contains("open")) toggleAssignToolbar(); $("notes-toolbar").classList.toggle("open", open); $("btn-notes").classList.toggle("active", open); if (!open) stopDrawing(); renderPalette(); } function renderPalette() { var host = $("notes-colors"); if (host.childElementCount) { host.querySelectorAll(".swatch").forEach(function (b) { b.classList.toggle("selected", b.getAttribute("data-color") === state.draw.color); }); return; } Notes.PALETTE.forEach(function (c) { var b = document.createElement("button"); b.className = "swatch" + (c.hex === state.draw.color ? " selected" : ""); b.style.background = c.hex; b.title = c.name; b.setAttribute("data-color", c.hex); b.addEventListener("click", function () { state.draw.color = c.hex; renderPalette(); }); host.appendChild(b); }); } function setDrawMode(mode) { finishTempLine(false); state.draw.mode = state.draw.mode === mode ? null : mode; document.querySelectorAll("#notes-toolbar [data-mode]").forEach(function (b) { b.classList.toggle("selected", b.getAttribute("data-mode") === state.draw.mode); }); $("notes-finish").style.display = (state.draw.mode === "line" || state.draw.mode === "area") ? "" : "none"; if (state.draw.mode) { var hints = { point: "הקישו על המפה להוספת נקודה", line: "הקישו נקודות לאורך הקו, ואז 'סיום קו'", area: "הקישו את פינות השטח, ואז 'סיום קו'", text: "הקישו על המפה במקום שבו יופיע הטקסט", }; toast(hints[state.draw.mode]); } } function stopDrawing() { finishTempLine(false); state.draw.mode = null; document.querySelectorAll("#notes-toolbar [data-mode]").forEach(function (b) { b.classList.remove("selected"); }); } function notesRecord(create) { var rec = state.files.find(function (f) { return f.id === Notes.NOTES_ID; }); if (!rec && create) { rec = Notes.newRecord(); state.files.push(rec); $("empty-hint").classList.add("hidden"); } return rec; } function saveNotes(rec) { Notes.recalcBbox(rec); Store.saveFile(rec); attachOverlay(rec); syncFileToGovmap(rec); } function addNoteEntity(entity) { var rec = notesRecord(true); rec.entities.push(entity); saveNotes(rec); } function handleDrawClick(ev) { var itm; try { itm = Geo.wgsToItm([ev.latlng.lat, ev.latlng.lng]); } catch (e) { toast("שגיאת המרה"); return; } var d = state.draw; if (d.mode === "point") { addNoteEntity(Notes.makePoint(itm, d.color)); } else if (d.mode === "text") { var text = prompt("טקסט ההערה:"); if (text && text.trim()) addNoteEntity(Notes.makeText(itm, text.trim(), d.color)); } else if (d.mode === "line" || d.mode === "area") { d.temp.push(itm); d.tempLatLngs.push(ev.latlng); if (!d.tempLayer) { d.tempLayer = L.polyline(d.tempLatLngs, { color: d.color, weight: 3, dashArray: "6 4" }).addTo(state.map); } else { d.tempLayer.setLatLngs(d.tempLatLngs); } } } function finishTempLine(commit) { var d = state.draw; if (d.tempLayer) { state.map.removeLayer(d.tempLayer); d.tempLayer = null; } if (commit && d.temp.length >= 2) { addNoteEntity(Notes.makePoly(d.temp.slice(), d.mode === "area", d.color)); } else if (commit) { toast("צריך לפחות שתי נקודות"); } d.temp = []; d.tempLatLngs = []; } function undoNote() { var d = state.draw; if (d.temp.length) { d.temp.pop(); d.tempLatLngs.pop(); if (d.tempLayer) d.tempLayer.setLatLngs(d.tempLatLngs); return; } var rec = notesRecord(false); if (rec && rec.entities.length) { rec.entities.pop(); saveNotes(rec); renderFilesPanel(); } else { toast("אין מה לבטל"); } } function exportNotes() { var rec = notesRecord(false); if (!rec || !rec.entities.length) { toast("שכבת ההערות ריקה"); return; } Notes.exportAndShare(rec).then(function () { toast("קובץ ה-DXF נוצר — נפתח ב-AutoCAD במיקום הנכון (רשת ישראל)", 4500); }).catch(function () { toast("הייצוא נכשל"); }); } // ---------- שיוך נכסים לבלוקי DRAFT ---------- function toggleAssignToolbar() { if (inGovmap()) { toast("שיוך נכסים זמין על רקעי OSM/לוויין", 4000); return; } var open = !$("assign-toolbar").classList.contains("open"); if (open && $("notes-toolbar").classList.contains("open")) toggleNotesToolbar(); $("assign-toolbar").classList.toggle("open", open); $("btn-assign").classList.toggle("active", open); if (!open) stopAssign(); } function stopAssign() { state.assign.mode = null; setAssignSelection(null); document.querySelectorAll("#assign-toolbar [data-amode]").forEach(function (b) { b.classList.remove("selected"); }); } function setAssignMode(mode) { var a = state.assign; a.mode = a.mode === mode ? null : mode; if (a.mode !== "link") setAssignSelection(null); document.querySelectorAll("#assign-toolbar [data-amode]").forEach(function (b) { b.classList.toggle("selected", b.getAttribute("data-amode") === a.mode); }); if (a.mode === "block") toast("הקישו על המפה למיקום בלוק DRAFT"); else if (a.mode === "link") toast("הקישו על בלוק DRAFT לבחירה, ואז על הנכסים"); else if (a.mode === "addr") toast("הקישו על הבניין שאינו ברשימות — ותקבלו חלון להקלדת הכתובת"); } function assignRecord(create) { var rec = state.files.find(function (f) { return f.id === Assign.ASSIGN_ID; }); if (!rec && create) { rec = Assign.newRecord(); state.files.push(rec); $("empty-hint").classList.add("hidden"); } return rec; } function saveAssign(rec) { Assign.rebuild(rec); Store.saveFile(rec); attachOverlay(rec); syncFileToGovmap(rec); } /** עיגול הדגשה סביב הבלוק הנבחר */ function setAssignSelection(draft) { var a = state.assign; a.sel = draft; if (a.selMarker) { state.map.removeLayer(a.selMarker); a.selMarker = null; } if (draft) { var ll = Geo.itmToWgs(draft.pt); a.selMarker = L.circleMarker(ll, { radius: 12, color: "#e53935", weight: 3, fill: false, interactive: false, }).addTo(state.map); } } function handleAssignClick(ev) { var itm; try { itm = Geo.wgsToItm([ev.latlng.lat, ev.latlng.lng]); } catch (e) { toast("שגיאת המרה"); return; } var a = state.assign; if (a.mode === "block") { var rec = assignRecord(true); rec.drafts.push({ pt: itm, auto: false, used: false }); rec.ops.push("draft"); saveAssign(rec); toast("בלוק DRAFT נוסף (" + rec.drafts.length + ")"); return; } if (a.mode === "addr") { // כתובת שלא קיימת ברשימות — טקסט על ADDR_NAME_NEW; הזהות שלה // בכל הצנרת (שיוך, CONFIG, השלמת הטבלה) היא הכתובת עצמה var txt = prompt("כתובת חדשה (רחוב ומספר):"); if (!txt || !txt.trim()) return; var recA = assignRecord(true); recA.newAddrs = recA.newAddrs || []; recA.newAddrs.push({ pt: itm, text: txt.trim().replace(/\s+/g, " ") }); recA.ops.push("addr"); saveAssign(recA); toast("כתובת חדשה נוספה: " + txt.trim() + " (" + recA.newAddrs.length + ")"); return; } if (a.mode === "link") { var rec2 = assignRecord(false); if (!rec2 || !rec2.drafts.length) { toast("אין בלוקי DRAFT — פזרו קודם בלוקים"); return; } // הקשה ליד בלוק — בחירה/החלפת בלוק (כמו Enter+בחירה ב-OPTASGN) var d = Assign.draftNear(rec2, itm); if (d && d !== a.sel) { setAssignSelection(d); toast("בלוק נבחר — הקישו על הנכסים לשיוך"); return; } if (!a.sel) { toast("הקישו קודם על בלוק DRAFT לבחירה"); return; } // הקשה על נכס — הצמדה לטקסט הכתובת הקרוב var hit = Assign.addrNear(state.files, itm); if (!hit) { toast("אין טקסט כתובת בטווח " + Assign.SNAP_ADDR + " מ' — התקרבו והקישו על הנכס"); return; } rec2.links.push({ a: hit.pt, b: a.sel.pt }); a.sel.used = true; rec2.ops.push("link"); saveAssign(rec2); toast("שויך: " + (hit.label || "נכס") + " (" + rec2.links.length + ")"); } } function scatterAssignAuto() { var rec = assignRecord(true); var n = Assign.scatterNearPoles(rec, state.files); if (!n) { toast("לא נמצאו עמודים/ארונות חדשים במפות הטעונות (POLE/ZPOLE/ZBOX)"); return; } saveAssign(rec); toast(n + " בלוקי DRAFT פוזרו ליד עמודים/ארונות. בייצוא ייכללו רק אלו ששויכו", 5000); } function undoAssign() { var rec = assignRecord(false); if (!rec || !rec.ops.length) { toast("אין מה לבטל"); return; } var op = rec.ops.pop(); if (op === "link") { rec.links.pop(); // אם לבלוק לא נשארו שיוכים — מסמנים כלא-בשימוש rec.drafts.forEach(function (d) { d.used = rec.links.some(function (l) { return l.b[0] === d.pt[0] && l.b[1] === d.pt[1]; }); }); } else if (op === "draft") { var d2 = rec.drafts.pop(); if (state.assign.sel === d2) setAssignSelection(null); } else if (op === "addr") { (rec.newAddrs || []).pop(); } else if (op && op.auto) { rec.drafts.splice(rec.drafts.length - op.auto, op.auto); setAssignSelection(null); } saveAssign(rec); renderFilesPanel(); } function exportAssign() { var rec = assignRecord(false); if (!rec || (!rec.drafts.length && !rec.links.length && !(rec.newAddrs || []).length)) { toast("שכבת השיוך ריקה"); return; } Assign.exportAndShare(rec).then(function (built) { if (!built) return; var msg = "יוצא DXF: " + built.nDrafts + " בלוקים, " + rec.links.length + " שיוכים"; if (built.nAddrs) msg += ", " + built.nAddrs + " כתובות חדשות"; if (built.nSkipped) msg += " (" + built.nSkipped + " בלוקים אוטומטיים לא-משויכים הושמטו)"; toast(msg, 5000); }).catch(function () { toast("הייצוא נכשל"); }); } // ---------- מתחמים מתוכננים (מכלי התכנון design.) ---------- var DESIGN_URL = /^gis\./.test(location.host) ? location.protocol + "//" + location.host.replace(/^gis\./, "design.") : null; var planned = []; // [{slug, name, planned, bbox, files}] function loadPlanned() { if (!DESIGN_URL) return; fetch(DESIGN_URL + "/api/compounds").then(function (r) { return r.json(); }) .then(function (list) { planned = list.filter(function (c) { return c.planned || c.stats; }); renderFilesPanel(); }).catch(function () { /* כלי התכנון לא זמין — מדלגים בשקט */ }); } function plannedSection(host) { if (!planned.length) return; var title = document.createElement("h3"); title.textContent = "מתחמים מתוכננים (בשרת)"; title.style.cssText = "font-size:14px;margin:14px 0 6px;color:#1565c0"; host.appendChild(title); planned.forEach(function (c) { var el = document.createElement("div"); el.className = "file-item"; var final = (c.files || []).filter(function (f) { return /^FINAL_.*\.dxf$/i.test(f); })[0]; el.innerHTML = '
' + escapeHtml(c.name || c.slug) + "" + 'תוכנן ' + escapeHtml(c.planned || "") + "
" + '
' + (c.bbox ? '' : "") + (final ? '' : "") + "
"; var pz = el.querySelector(".pzoom"); if (pz) pz.addEventListener("click", function () { var ll = Geo.itmToWgs([(c.bbox[0] + c.bbox[2]) / 2, (c.bbox[1] + c.bbox[3]) / 2]); closePanels(); state.map.setView(ll, 15); L.circleMarker(ll, { radius: 14, color: "#1565c0", weight: 3, fill: false }) .addTo(state.map).bindPopup(escapeHtml(c.name || c.slug)).openPopup(); }); var pl = el.querySelector(".pload"); if (pl) pl.addEventListener("click", function () { pl.disabled = true; pl.textContent = "טוען…"; fetch(DESIGN_URL + "/api/compounds/" + encodeURIComponent(c.slug) + "/files/" + encodeURIComponent(final)) .then(function (r) { return r.blob(); }) .then(function (b) { addFile(new File([b], (c.name || c.slug) + ".dxf")); closePanels(); }) .catch(function () { toast("טעינת המפה נכשלה"); }) .then(function () { pl.disabled = false; pl.textContent = "טעינת המפה"; }); }); host.appendChild(el); }); } // ---------- הוספת קבצים ---------- function addFiles(fileList) { Array.prototype.slice.call(fileList).forEach(function (f) { addFile(f); }); } function addFile(f) { if (/\.dwg$/i.test(f.name)) { toast("קובץ DWG הוא פורמט בינארי סגור — יש לשמור אותו כ-DXF ‏(SAVEAS בפורמט DXF) ולהעלות שוב", 6000); return; } loading(true, "מפענח את " + f.name + "…"); var reader = new FileReader(); reader.onerror = function () { loading(false); toast("שגיאה בקריאת הקובץ"); }; reader.onload = function () { // הפענוח רץ ב-Web Worker — הממשק לא קופא גם בקבצים כבדים DXF.parseAsync(reader.result).then(function (parsed) { // זיהוי לפי רוב + סינון רשומות עם קואורדינטות שבורות var clean = Geo.sanitizeParsed(parsed); var isItm = clean.coordSys === "itm"; var rec = { id: "f" + Date.now() + "_" + Math.floor(Math.random() * 1e6), name: f.name, addedAt: new Date().toISOString(), entities: clean.entities, bbox: clean.bbox, coordSys: clean.coordSys, calibration: null, opacity: 1, weight: 2, visible: true, hiddenLayers: [], }; state.files.push(rec); Store.saveFile(rec).catch(function () { toast("הקובץ נטען אך לא נשמר לאחסון המקומי (ייתכן שגדול מדי)"); }); attachOverlay(rec); syncFileToGovmap(rec); zoomToFile(rec); $("empty-hint").classList.add("hidden"); loading(false); if (isItm) { toast("‏" + f.name + " זוהה ברשת ישראל והולבש על המפה (" + clean.entities.length + " ישויות" + (clean.dropped ? "; סוננו " + clean.dropped + " רשומות עם קואורדינטות שגויות" : "") + ")", clean.dropped ? 5000 : 2600); } else { toast("הקואורדינטות בקובץ אינן ברשת ישראל — מוקם זמנית במרכז המפה. השתמשו ב'כיול' למיקום מדויק", 6000); } }).catch(function (err) { loading(false); var msg = err && err.message ? err.message : ""; if (msg === "DWG_BINARY") toast("זהו קובץ DWG בינארי — יש להמיר ל-DXF (למשל SAVEAS ב-AutoCAD או ODA File Converter)", 6000); else if (msg === "NOT_DXF") toast("הקובץ אינו DXF תקין"); else if (msg === "EMPTY_DXF") toast("לא נמצאו ישויות ניתנות להצגה בקובץ"); else toast("שגיאה בפענוח: " + msg, 5000); }); }; reader.readAsArrayBuffer(f); } // ---------- שכבות סטטוס (נתונים חיים מהשרת) ---------- function toggleStatusLayer(key, on) { StatusLayers.setEnabled(key, on); var id = "status_" + key; if (!on) { detachOverlay(id); GovMap.clearFile(id); state.files = state.files.filter(function (x) { return x.id !== id; }); renderFilesPanel(); return; } StatusLayers.fetchData().then(function () { var layer = StatusLayers.layerByKey(key); if (!layer) { toast("השכבה לא קיימת בשרת"); return; } if (!layer.count) { toast("‏" + layer.name + ": אין כרגע נכסים בסטטוס הזה"); } var rec = StatusLayers.buildRecord(layer); var idx = state.files.findIndex(function (x) { return x.id === rec.id; }); if (idx >= 0) state.files[idx] = rec; else state.files.push(rec); attachOverlay(rec); syncFileToGovmap(rec); if (layer.count) toast("‏" + layer.name + ": " + layer.count.toLocaleString() + " נכסים על המפה"); renderFilesPanel(); }).catch(function () { StatusLayers.setEnabled(key, false); renderFilesPanel(); toast("לא ניתן למשוך את נתוני הסטטוס מהשרת — בדקו חיבור", 4500); }); } function restoreStatusLayers() { var keys = StatusLayers.enabledKeys(); if (!keys.length) return; StatusLayers.fetchData().then(function () { keys.forEach(function (k) { toggleStatusLayer(k, true); }); }).catch(function () { /* אין רשת — יידלקו בפעם הבאה */ }); } function renderStatusSection(host) { var title = document.createElement("div"); title.className = "base-section-title"; title.textContent = "שכבות סטטוס — חי מהשרת"; host.appendChild(title); var known = StatusLayers.layers(); var enabled = StatusLayers.enabledKeys(); if (!known.length) { // עוד לא נמשכו נתונים — מציגים כפתור טעינה var btn = document.createElement("button"); btn.className = "status-load-btn"; btn.textContent = "טעינת שכבות סטטוס מהשרת"; btn.addEventListener("click", function () { btn.textContent = "מושך נתונים…"; StatusLayers.fetchData().then(function () { renderFilesPanel(); }) .catch(function () { btn.textContent = "טעינת שכבות סטטוס מהשרת"; toast("לא ניתן למשוך את נתוני הסטטוס — בדקו חיבור", 4500); }); }); host.appendChild(btn); return; } known.forEach(function (l) { var row = document.createElement("label"); row.className = "status-row"; var on = enabled.indexOf(l.key) !== -1; row.innerHTML = '" + '' + '' + escapeHtml(l.name) + "" + '' + (l.count || 0).toLocaleString() + ""; row.querySelector("input").addEventListener("change", function (ev) { toggleStatusLayer(l.key, ev.target.checked); }); host.appendChild(row); }); if (StatusLayers.generatedAt()) { var meta = document.createElement("div"); meta.className = "status-meta"; try { meta.textContent = "עודכן בשרת: " + new Date(StatusLayers.generatedAt()).toLocaleString("he-IL"); } catch (e) { /* תאריך לא תקין */ } host.appendChild(meta); } } // ---------- פאנל קבצים ---------- function renderFilesPanel() { var host = $("files-list"); host.innerHTML = ""; renderStatusSection(host); var files = state.files.filter(function (f) { return !f.isStatus; }); var filesTitle = document.createElement("div"); filesTitle.className = "base-section-title"; filesTitle.textContent = "שכבות DXF"; host.appendChild(filesTitle); if (!files.length) { var p = document.createElement("p"); p.style.cssText = "color:#777;font-size:14px;text-align:center;padding:14px 0"; p.textContent = "אין קבצים. לחצו + להוספת DXF."; host.appendChild(p); plannedSection(host); return; } files.forEach(function (f) { var el = document.createElement("div"); el.className = "file-item"; var badge = f.isNotes ? 'הערות שלי' : f.isAssign ? 'שיוך נכסים' : f.shared ? 'שכבה ארצית' : f.coordSys === "itm" ? 'רשת ישראל' : f.coordSys === "calibrated" ? 'מכויל ידנית' : 'דורש כיול'; var ovf = state.overlays[f.id] || {}; var layerNames = Object.keys(ovf.byLayer || {}); (ovf.texts || []).forEach(function (t) { if (layerNames.indexOf(t.layer) === -1) layerNames.push(t.layer); }); var sub = ""; if (layerNames.length > 1) { // מצב פתוח/סגור נשמר — הפאנל נבנה מחדש בכל שינוי שכבה, ובלי // זה הרשימה הייתה נסגרת אחרי כל כיבוי (שכבה אחת בכל פעם) sub = '
שכבות בשרטוט (' + layerNames.length + ")" + layerNames.map(function (n) { var hidden = (f.hiddenLayers || []).indexOf(n) !== -1; return '"; }).join("") + "
"; } el.innerHTML = '
' + '" + '' + escapeHtml(f.name) + "" + badge + "
" + '
שקיפות
' + sub + '
' + '' + (f.isNotes || f.isAssign ? '' : f.shared ? "" : '' + (f.coordSys === "calibrated" ? '' : "") + '') + "
"; el.querySelector(".vis-toggle").addEventListener("change", function (ev) { f.visible = ev.target.checked; Store.saveFile(f); if (f.visible) attachOverlay(f); else detachOverlay(f.id); syncFileToGovmap(f); renderFilesPanel(); }); el.querySelector(".opacity").addEventListener("change", function (ev) { f.opacity = parseInt(ev.target.value, 10) / 100; Store.saveFile(f); attachOverlay(f); syncFileToGovmap(f); }); el.querySelector(".zoom").addEventListener("click", function () { if (!f.bbox) { toast("השכבה ריקה"); return; } closePanels(); zoomToFile(f); }); var calBtn = el.querySelector(".calibrate"); if (calBtn) calBtn.addEventListener("click", function () { startCalibration(f); }); var uncal = el.querySelector(".uncal"); if (uncal) uncal.addEventListener("click", function () { f.calibration = null; f.coordSys = Geo.looksLikeITM(f.bbox) ? "itm" : "unplaced"; f._anchorItm = null; Store.saveFile(f); attachOverlay(f); syncFileToGovmap(f); toast("הכיול אופס"); }); var del = el.querySelector(".delete"); if (del) del.addEventListener("click", function () { if (!confirm("למחוק את " + f.name + "?")) return; detachOverlay(f.id); GovMap.clearFile(f.id); state.files = state.files.filter(function (x) { return x.id !== f.id; }); Store.deleteFile(f.id); renderFilesPanel(); if (!state.files.length) $("empty-hint").classList.remove("hidden"); }); var exp = el.querySelector(".export"); if (exp) exp.addEventListener("click", f.isAssign ? exportAssign : exportNotes); var clr = el.querySelector(".clear-notes"); if (clr) clr.addEventListener("click", function () { if (!confirm(f.isAssign ? "לנקות את כל השיוכים והבלוקים?" : "לנקות את כל ההערות?")) return; f.entities = []; if (f.isAssign) { f.drafts = []; f.links = []; f.newAddrs = []; f.ops = []; setAssignSelection(null); saveAssign(f); } else { saveNotes(f); } renderFilesPanel(); }); var det = el.querySelector("details.dxf-sublayers"); if (det) det.addEventListener("toggle", function () { f._layersOpen = det.open; }); el.querySelectorAll("[data-sublayer]").forEach(function (cb) { cb.addEventListener("change", function (ev) { var name = ev.target.getAttribute("data-sublayer"); f.hiddenLayers = f.hiddenLayers || []; if (ev.target.checked) { f.hiddenLayers = f.hiddenLayers.filter(function (x) { return x !== name; }); } else if (f.hiddenLayers.indexOf(name) === -1) { f.hiddenLayers.push(name); } Store.saveFile(f); attachOverlay(f); }); }); host.appendChild(el); }); plannedSection(host); } function escapeHtml(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } function escapeAttr(s) { return escapeHtml(s).replace(/"/g, """); } // ---------- כיול ידני (2 נקודות) ---------- function startCalibration(file) { if (inGovmap()) { toast("כיול זמין כרגע על רקעי OSM/לוויין — החליפו רקע, כיילו, וחזרו ל-govmap", 5000); return; } closePanels(); cancelCalibration(); if (file.visible === false) { file.visible = true; attachOverlay(file); } state.cal = { fileId: file.id, pairs: [], pins: [], step: "pickSrc" }; updateCalBanner(); $("calibrate-banner").classList.add("open"); } function updateCalBanner() { var cal = state.cal; if (!cal) return; var n = cal.pairs.length + 1; $("calibrate-text").innerHTML = cal.step === "pickSrc" ? "כיול — נקודה " + n + "/2: הקישו על נקודה מוכרת בשרטוט (פינת מבנה, שוחה, עמוד…)" : "כיול — נקודה " + n + "/2: כעת הקישו על המיקום האמיתי של אותה נקודה על המפה"; } function cancelCalibration() { if (!state.cal) return; state.cal.pins.forEach(function (p) { state.map.removeLayer(p); }); state.cal = null; $("calibrate-banner").classList.remove("open"); } function calPin(latlng, label) { var m = L.marker(latlng, { icon: L.divIcon({ className: "", html: '
' + label + "
", iconSize: [22, 22], iconAnchor: [11, 11] }), interactive: false, }).addTo(state.map); state.cal.pins.push(m); } function onLeafletClick(ev) { var cal = state.cal; if (!cal) { if (state.assign.mode) handleAssignClick(ev); else if (state.draw.mode) handleDrawClick(ev); return; } var file = state.files.find(function (f) { return f.id === cal.fileId; }); var ov = state.overlays[cal.fileId]; if (!file || !ov) { cancelCalibration(); return; } if (cal.step === "pickSrc") { var hit = Render.nearestVertex(file, ov.transform, ev.latlng, state.map, 50); if (!hit) { toast("לא נמצא קודקוד שרטוט קרוב — התקרבו והקישו על קו בשרטוט"); return; } cal.currentSrc = hit.src; calPin(hit.latlng, cal.pairs.length + 1); cal.step = "pickDst"; updateCalBanner(); } else { var dstItm; try { dstItm = Geo.wgsToItm([ev.latlng.lat, ev.latlng.lng]); } catch (e) { toast("שגיאת המרה"); return; } cal.pairs.push({ src: cal.currentSrc, dstItm: dstItm }); calPin(ev.latlng, "✓"); cal.step = "pickSrc"; updateCalBanner(); if (cal.pairs.length >= 2) finishCalibration(file); } } function finishCalibration(file) { var p = state.cal.pairs; var h = Geo.helmertFrom2(p[0].src, p[0].dstItm, p[1].src, p[1].dstItm); cancelCalibration(); if (!h) { toast("הנקודות קרובות מדי זו לזו — נסו שוב עם נקודות מרוחקות יותר"); return; } file.calibration = h.params; file.coordSys = "calibrated"; file._anchorItm = null; Store.saveFile(file); attachOverlay(file); syncFileToGovmap(file); zoomToFile(file); toast("הכיול הושלם (קנה מידה " + h.scale.toFixed(3) + ", סיבוב " + h.rotationDeg.toFixed(1) + "°)"); } // ---------- מיקום ---------- function toggleLocate() { if (inGovmap()) { var on = GovMap.toggleGps(); $("btn-locate").classList.toggle("active", on); return; } var loc = state.loc; if (loc.watchId !== null) { navigator.geolocation.clearWatch(loc.watchId); loc.watchId = null; if (loc.marker) { state.map.removeLayer(loc.marker); loc.marker = null; } if (loc.circle) { state.map.removeLayer(loc.circle); loc.circle = null; } $("btn-locate").classList.remove("active"); return; } if (!navigator.geolocation) { toast("אין תמיכה ב-GPS בדפדפן זה"); return; } $("btn-locate").classList.add("active"); loc.follow = true; loc.watchId = navigator.geolocation.watchPosition(function (pos) { var latlng = [pos.coords.latitude, pos.coords.longitude]; var acc = pos.coords.accuracy || 20; if (!loc.marker) { loc.marker = L.marker(latlng, { icon: L.divIcon({ className: "", html: '
', iconSize: [16, 16], iconAnchor: [8, 8] }), interactive: false, }).addTo(state.map); loc.circle = L.circle(latlng, { radius: acc, weight: 1, color: "#1a73e8", fillOpacity: 0.08, interactive: false }).addTo(state.map); state.map.setView(latlng, Math.max(state.map.getZoom(), 17)); } else { loc.marker.setLatLng(latlng); loc.circle.setLatLng(latlng).setRadius(acc); if (loc.follow) state.map.panTo(latlng); } }, function (err) { toast("שגיאת מיקום: " + (err.code === 1 ? "אין הרשאה" : err.message)); $("btn-locate").classList.remove("active"); loc.watchId = null; }, { enableHighAccuracy: true, maximumAge: 3000, timeout: 15000 }); } // ---------- פאנלים: מפת רקע והגדרות ---------- function renderBasemapPanel() { var host = $("basemap-list"); host.innerHTML = ""; var gmTitle = document.createElement("div"); gmTitle.className = "base-section-title"; gmTitle.textContent = "govmap — פורטל המפות הממשלתי"; host.appendChild(gmTitle); var cfg = GovMap.config(); GovMap.BACKGROUNDS.forEach(function (bg) { var div = document.createElement("div"); var selected = inGovmap() && cfg.background === bg.id; div.className = "base-option" + (selected ? " selected" : ""); var note = GovMap.hasToken() ? "" : '
דורש טוקן API — ראו הגדרות ⚙
'; div.innerHTML = "
" + bg.name + "
" + note + "
"; div.addEventListener("click", function () { closePanels(); enterGovmapMode(bg.id); }); host.appendChild(div); }); var lfTitle = document.createElement("div"); lfTitle.className = "base-section-title"; lfTitle.textContent = "מפות רקע כלליות (עובדות גם אופליין)"; host.appendChild(lfTitle); Object.keys(Basemaps.list).forEach(function (key) { var def = Basemaps.list[key]; var div = document.createElement("div"); div.className = "base-option" + (!inGovmap() && key === state.baseKey ? " selected" : ""); var note = ""; if (key === "googleStreets" || key === "googleHybrid") { note = '
שימוש באריחי Google כפוף לתנאי השירות של Google
'; } div.innerHTML = "
" + def.name + "
" + note + "
"; div.addEventListener("click", function () { setLeafletBase(key); closePanels(); }); host.appendChild(div); }); } function loadSettingsForm() { $("gm-token").value = GovMap.config().token || ""; } function saveSettingsForm() { var cfg = GovMap.config(); var wasCreated = GovMap.isCreated(); cfg.token = $("gm-token").value.trim(); GovMap.saveConfig(cfg); closePanels(); renderBasemapPanel(); if (!cfg.token) { toast("הטוקן נוקה"); return; } if (wasCreated) toast("הטוקן נשמר — רעננו את הדף כדי להחיל אותו על המפה", 4500); else toast("הטוקן נשמר — בחרו רקע govmap מתפריט המפות"); } // ---------- אתחול ---------- function init() { buildMap(DEFAULT_CENTER, DEFAULT_ZOOM); var pref = state.baseKey; if (pref.indexOf("govmap:") === 0) { // מתחילים על OSM; govmap ייטען אחרי שיש קבצים ומיקום התחלתי state.baseKey = "osm"; setLeafletBase("osm"); if (GovMap.hasToken() && navigator.onLine) { setTimeout(function () { enterGovmapMode(pref.split(":")[1] || "0"); }, 400); } } else { setLeafletBase(pref); } Store.listFiles().then(function (files) { state.files = files || []; if (state.files.length) { $("empty-hint").classList.add("hidden"); state.files.forEach(function (f) { attachOverlay(f); }); var lastUser = state.files.filter(function (f) { return !f.shared && !f.isNotes && !f.isAssign && f.bbox; }).pop(); if (!inGovmap() && lastUser) zoomToFile(lastUser); if (inGovmap()) pushAllToGovmap(); } renderFilesPanel(); // שכבות ארציות מהשרת — נטענות/מתעדכנות ברקע return SharedLayers.sync(state.files).then(function (res) { res.removedIds.forEach(function (id) { detachOverlay(id); GovMap.clearFile(id); state.files = state.files.filter(function (x) { return x.id !== id; }); Store.deleteFile(id); }); res.records.forEach(function (rec) { var idx = state.files.findIndex(function (x) { return x.id === rec.id; }); if (idx >= 0) state.files[idx] = rec; else state.files.push(rec); attachOverlay(rec); syncFileToGovmap(rec); }); if (state.files.length) $("empty-hint").classList.add("hidden"); renderFilesPanel(); }); }).then(function () { // שכבות סטטוס שהיו דלוקות — נמשכות טריות מהשרת restoreStatusLayers(); }).catch(function () { renderFilesPanel(); }); // כפתורים $("btn-add").addEventListener("click", function () { $("file-input").click(); }); $("file-input").addEventListener("change", function (ev) { addFiles(ev.target.files); ev.target.value = ""; }); $("btn-layers").addEventListener("click", function () { renderFilesPanel(); openPanel("panel-layers"); }); $("btn-basemap").addEventListener("click", function () { renderBasemapPanel(); openPanel("panel-basemap"); }); $("btn-settings").addEventListener("click", function () { loadSettingsForm(); openPanel("panel-settings"); }); $("btn-locate").addEventListener("click", toggleLocate); $("btn-labels").addEventListener("click", function () { state.labelsOn = !state.labelsOn; $("btn-labels").classList.toggle("active", state.labelsOn); updateLabelVisibility(); if (inGovmap()) pushAllToGovmap(); toast(state.labelsOn ? "טקסטים מהשרטוט מוצגים (בזום גבוה)" : "טקסטים מהשרטוט הוסתרו"); }); $("btn-labels").classList.add("active"); // חיפוש $("btn-search").addEventListener("click", openSearch); $("search-close").addEventListener("click", closeSearch); $("search-go").addEventListener("click", runSearch); $("search-input").addEventListener("keydown", function (ev) { if (ev.key === "Enter") runSearch(); }); // שכבת הערות $("btn-notes").addEventListener("click", toggleNotesToolbar); $("btn-assign").addEventListener("click", toggleAssignToolbar); document.querySelectorAll("#assign-toolbar [data-amode]").forEach(function (b) { b.addEventListener("click", function () { setAssignMode(b.getAttribute("data-amode")); }); }); $("assign-auto").addEventListener("click", scatterAssignAuto); $("assign-undo").addEventListener("click", undoAssign); $("assign-export").addEventListener("click", exportAssign); $("assign-close").addEventListener("click", toggleAssignToolbar); document.querySelectorAll("#notes-toolbar [data-mode]").forEach(function (b) { b.addEventListener("click", function () { setDrawMode(b.getAttribute("data-mode")); }); }); $("notes-finish").addEventListener("click", function () { finishTempLine(true); }); $("notes-undo").addEventListener("click", undoNote); $("notes-export").addEventListener("click", exportNotes); $("notes-close").addEventListener("click", toggleNotesToolbar); document.querySelectorAll(".panel-close").forEach(function (b) { b.addEventListener("click", closePanels); }); $("gm-save").addEventListener("click", saveSettingsForm); $("calibrate-cancel").addEventListener("click", cancelCalibration); $("calibrate-restart").addEventListener("click", function () { var cal = state.cal; if (!cal) return; var file = state.files.find(function (f) { return f.id === cal.fileId; }); if (file) startCalibration(file); }); // גרירת קבצים (דסקטופ) document.addEventListener("dragover", function (e) { e.preventDefault(); }); document.addEventListener("drop", function (e) { e.preventDefault(); if (e.dataTransfer && e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }); // Service Worker if ("serviceWorker" in navigator) { navigator.serviceWorker.register("sw.js").catch(function () { /* אופליין לא זמין */ }); } // מתחמים מתוכננים מכלי התכנון loadPlanned(); } document.addEventListener("DOMContentLoaded", init); })();