/**
* 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);
})();