let map, houseGroup, homesteadGroup, buildingGroup, pointsGroup; let numApartments = 1; let mode = ''; const Constructor = { info: {}, async init() { let html = await fetch('/lib/pages/constructor/index.html').then((response) => response.text()); app.innerHTML = html; map = ""; houseGroup = ""; homesteadGroup = ""; buildingGroup = ""; pointsGroup = ""; numApartments = 1; this.info = { type: 'house', points: [], points_number: [], geo: {}, osm_id: [], zoom: 17, title: null, number: null, settlement: null } const infoTypeInputs = document.querySelectorAll('input[name="info-type"]'); const infoForm = document.getElementById('info-form'); const infoLabels = { points: { title: "Назва", number: "Номер", settlement: "Місто", init: () => Constructor.points.init(), next: () => Constructor.points.next(), save: () => Constructor.points.save() }, homestead: { title: "Назва району / села", number: "Номер району", settlement: "Місто", init: () => Constructor.homestead.init(), next: () => Constructor.homestead.next(), save: () => Constructor.homestead.save() }, house: { title: "Назва вулиці", number: "Номер будинку", settlement: "Місто", init: () => Constructor.house.init(), next: () => Constructor.house.next(), save: () => Constructor.house.save() } }; let currentInit = infoLabels['house'].init; let currentNext = infoLabels['house'].next; let currentSave = infoLabels['house'].save; function renderInfoForm(type) { const labels = infoLabels[type]; currentInit = labels.init; currentNext = labels.next; currentSave = labels.save; infoForm.innerHTML = `
`; } // Обработчик submit формы infoForm.addEventListener('submit', (event) => { event.preventDefault(); document.getElementById('part-1-button').style.display = "none"; ['title', 'number', 'settlement'].forEach(key => { Constructor.info[key] = document.getElementById(`info-${key}`).value; }); if (currentInit) currentInit(); }); // Слушатели радиокнопок infoTypeInputs.forEach(radio => { radio.addEventListener('change', event => { const value = event.target.value; const part_2 = document.getElementById('part-2'); const part_2_button = document.getElementById('part-2-button'); part_2.style.display = "none"; part_2_button.style.display = ""; const part_3 = document.getElementById('part-3'); part_3.style.display = "none"; renderInfoForm(value); console.log(`Вибрано: ${value}`); this.info.type = value; this.info.osm_id = null; this.info.geo = {}; this.info.points_number = []; this.info.points = []; this.house.apartments.quantity = 0; this.house.apartments.list = []; }); }); document.getElementById('part-2-button').addEventListener('click', (event) => { event.preventDefault(); document.getElementById('part-2-button').style.display = "none"; if (currentNext) currentNext(); }); document.getElementById('part-3-button').addEventListener('click', (event) => { event.preventDefault(); // document.getElementById('part-3-button').style.display = "none"; if (currentSave) currentSave(); }); }, points: { init() { console.log('points'); // const part_2 = document.getElementById('part-2'); // const part_2_title = document.getElementById('part-2-title'); // part_2_title.innerHTML = `Крок 2. Створення точок на карті`; // part_2.style.display = ""; }, next() { console.log('points next'); }, save() { console.log('points next save'); } }, homestead: { init() { console.log('homestead'); const part_2 = document.getElementById('part-2'); const part_2_title = document.getElementById('part-2-title'); part_2_title.innerHTML = `Крок 2. Створення ділянки`; part_2.style.display = ""; Constructor.osm.init(); }, next() { console.log('homestead next'); const part_3 = document.getElementById('part-3'); const title = part_3.querySelector('h1'); const button = part_3.querySelector('#part-3-button'); part_3.innerHTML = ''; title.innerHTML = `Крок 3. Створення будинків`; part_3.appendChild(title); part_3.innerHTML += `

*Натисніть кнопку нижче, а потім клацайте на карті, щоб додати будинки. Після цього натисніть "Зберегти".

*Щоб видалити будинок, клацніть на ньому та у спливаючому вікні оберіть "Видалити".


`; part_3.appendChild(button); part_3.style.display = ""; this.building.init(); }, async save() { Constructor.info.buildings = Constructor.homestead.building.list; console.log(Constructor.info); Constructor.save(); }, building: { list: [], editing: false, async init() { this.editing = false; setLeafletCursor('pointer'); // Обробник кліку на карту homesteadGroup.on('click', e => { console.log(this.editing); if (e.layer instanceof L.Marker || !this.editing) return; const { lat, lng } = e.latlng; console.log(`Координати: ${lat.toFixed(5)}, ${lng.toFixed(5)}`); setLeafletCursor('progress'); this.editing = false; this.addBuilding({ geo: e.latlng, title: this.list.length + 1 }); }); }, async addBuilding({ geo, title }) { this.list.push({ title: title, geo: geo }); // Додаємо маркер на карту const redDot = L.divIcon({ className: "leaflet_drop", html: `
`, iconSize: [16, 16], iconAnchor: [8, 8] }); const marker = L.marker(geo, { icon: redDot }).addTo(buildingGroup); marker.bindPopup(` Будинок: ${this.list.length}
Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
`); setLeafletCursor('crosshair'); this.editing = true; }, async delleteBuilding({ id }) { const el = document.getElementById(`redDot_${id}`); if (el) el.remove(); this.list = this.list.filter(item => item.title !== id); const block = document.getElementById(`Building_${id}`); if (block) block.remove(); houseGroup.eachLayer(layer => { if (layer instanceof L.Marker && layer.getPopup()?.getContent().includes(`Будинок: ${id}`)) { houseGroup.removeLayer(layer); } }); }, newHouse(element) { const btn = element; this.editing = !this.editing; setLeafletCursor(this.editing ? 'crosshair' : 'pointer'); btn.innerHTML = this.editing ? ` Завершити додавання` : ` Додати будинок`; if (this.editing) alert("Натискаючи на карту будуть створюватись нові точки (будинки)"); } } }, house: { init() { console.log('house'); const part_2 = document.getElementById('part-2'); const part_2_title = document.getElementById('part-2-title'); part_2_title.innerHTML = `Крок 2. Конструктор будинків`; part_2.style.display = ""; Constructor.osm.init(); }, next() { console.log('house next'); const part_3 = document.getElementById('part-3'); const title = part_3.querySelector('h1'); const button = part_3.querySelector('#part-3-button'); part_3.innerHTML = ''; title.innerHTML = `Крок 3. Конструктор квартир`; part_3.appendChild(title); part_3.innerHTML += `` part_3.appendChild(button); part_3.style.display = ""; this.apartments.init(); }, async save() { console.log('house next save'); Constructor.info.entrances = this.apartments.list.map((entrance, entranceIndex) => { let apartments = []; let apartmentCounter = 0; entrance.list.forEach((floor, floorIndex) => { floor.forEach(apartment => { apartments.push({ title: apartment.title, apartment_number: apartmentCounter++, floors_number: floorIndex + 1 }); }); }); return { title: entrance.title, entrance_number: entranceIndex, apartments }; }); Constructor.save(); }, apartments: { quantity: 0, list: [], init() { const part_3 = document.getElementById("part-3"); const part_3_Button = part_3.querySelector("#part-3-button"); this.quantity++; const newEntrance = { title: `Під'їзд ${this.list.length + 1}`, list: [[{ title: this.quantity }]] }; this.list.push(newEntrance); const eIndex = this.list.length - 1; const floorIndex = 0; const apartmentIndex = 0; const houseDiv = this.createHouse(eIndex); const entranceDiv = this.createEntrance(eIndex); const floorDiv = this.createFloor(eIndex, floorIndex); const apartmentDiv = this.createApartment(eIndex, floorIndex, apartmentIndex, this.quantity); floorDiv.insertBefore(apartmentDiv, floorDiv.querySelector(".floor-info")); entranceDiv.appendChild(floorDiv); houseDiv.insertBefore(entranceDiv, houseDiv.querySelector(".entrance-button")); part_3.insertBefore(houseDiv, part_3_Button); }, createApartment(entrance, floor, apartment, value) { const div = document.createElement("div"); div.className = "apartment"; div.id = `apartment-${entrance}-${floor}-${apartment}`; div.innerHTML = ` `; return div; }, createFloor(entrance, floor) { const div = document.createElement("div"); div.className = "floor"; div.id = `floor-${entrance}-${floor}`; div.innerHTML = `

Поверх ${floor + 1}

`; return div; }, createEntrance(entrance) { const div = document.createElement("div"); div.className = "entrance"; div.id = `entrance-${entrance}`; div.innerHTML = `
`; return div; }, createHouse() { const div = document.createElement("div"); div.id = `house`; div.innerHTML = ` `; return div; }, addEntrance() { const blockHouse = document.getElementById("house"); const houseButton = blockHouse.querySelector(".entrance-button"); this.editQuantity(this.quantity + 1); const newEntrance = { title: `Під'їзд ${this.list.length + 1}`, list: [[{ title: this.quantity }]] }; this.list.push(newEntrance); const eIndex = this.list.length - 1; const floorIndex = 0; const apartmentIndex = 0; const entranceDiv = this.createEntrance(eIndex); const floorDiv = this.createFloor(eIndex, floorIndex); const apartmentDiv = this.createApartment(eIndex, floorIndex, apartmentIndex, this.quantity); floorDiv.insertBefore(apartmentDiv, floorDiv.querySelector(".floor-info")); entranceDiv.appendChild(floorDiv); blockHouse.insertBefore(entranceDiv, houseButton); }, editEntrance(entrance, value) { this.list[entrance].title = value; }, addFloors(entrance) { const entranceBlock = document.getElementById(`entrance-${entrance}`); const entranceInfo = entranceBlock.querySelector(".entrance-info"); this.editQuantity(this.quantity + 1); this.list[entrance].list.push([{ title: this.quantity }]); const fIndex = this.list[entrance].list.length - 1; const floorDiv = this.createFloor(entrance, fIndex); const aptDiv = this.createApartment(entrance, fIndex, 0, this.quantity); floorDiv.insertBefore(aptDiv, floorDiv.querySelector(".floor-info")); entranceInfo.after(floorDiv); }, addApartment(entrance, floor) { const blockFloor = document.getElementById(`floor-${entrance}-${floor}`); const floorInfo = blockFloor.querySelector(".floor-info"); this.editQuantity(this.quantity + 1); this.list[entrance].list[floor].push({ title: this.quantity }); const aIndex = this.list[entrance].list[floor].length - 1; const aptDiv = this.createApartment(entrance, floor, aIndex, this.quantity); blockFloor.insertBefore(aptDiv, floorInfo); }, editApartment(entrance, floor, apartment, value) { this.list[entrance].list[floor][apartment].title = value; }, deleteApartment(entrance, floor, apartment) { this.list[entrance].list[floor].splice(apartment, 1); document.getElementById(`apartment-${entrance}-${floor}-${apartment}`)?.remove(); this.editQuantity(this.quantity - 1); }, editQuantity(value) { const next_apartment_title = document.getElementById('next-apartment-title'); next_apartment_title.style.display = ""; next_apartment_title.value = value; this.quantity = Number(value); } } }, osm: { init() { const center = { lat: 49.5629016, lng: 25.6145625 }; const zoom = 19; const googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { subdomains: ['mt0', 'mt1', 'mt2', 'mt3'] }); const osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); const mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', { maxZoom: 20, minZoom: 15, tms: true }); if (!map) { houseGroup = new L.FeatureGroup(); homesteadGroup = new L.FeatureGroup(); buildingGroup = new L.FeatureGroup(); pointsGroup = new L.FeatureGroup(); map = L.map('map', { renderer: L.canvas(), center, zoom, layers: [googleHybrid, osm, mytile, houseGroup, homesteadGroup, buildingGroup, pointsGroup], zoomControl: false }); L.control.layers( { "Google Hybrid": googleHybrid, "OpenStreetMap": osm, "Territory Map": mytile }, { "Багатоповерхові будинки": houseGroup, "Житлові райони": homesteadGroup, "Приватні будинки": buildingGroup, "Точки на карті": pointsGroup }, { position: 'bottomright' } ).addTo(map); map.pm.addControls({ position: 'bottomright', drawCircleMarker: false, drawPolyline: false, drawPolygon: false, drawRectangle: false, drawCircle: false, drawText: false, drawMarker: false, cutPolygon: false, tooltips: false, editMode: true, dragMode: true, }); map.pm.toggleControls() // Событие после завершения рисования map.on('pm:create', e => { const layer = e.layer; let LatLngs = layer.getLatLngs(); LatLngs[0].push(LatLngs[0][0]); Constructor.info.points.push(LatLngs); Constructor.info.points_number.push(this.center(layer.getLatLngs())); let geo = this.center(layer.getLatLngs()); const house = layer; // сохраняем именно слой if (Constructor.info.type === 'house') { houseGroup.addLayer(house); } else if (Constructor.info.type === 'homestead') { homesteadGroup.addLayer(house); } house.bindPopup(` Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
`); // при открытии popup вешаем обработчик удаления house.on('popupopen', (e) => { if (Constructor.homestead.building.editing) { house.closePopup(); return; } const btn = e.popup.getElement().querySelector('.map_dell'); if (btn) { btn.addEventListener('click', () => { Constructor.osm.delete(house); }); } }); Constructor.osm.autoZoom(Constructor.info.points); }); map.pm.setLang("ua"); } houseGroup.clearLayers(); homesteadGroup.clearLayers(); buildingGroup.clearLayers(); pointsGroup.clearLayers(); }, newPoligon() { if (Constructor.info.type === 'house') { map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, layerGroup: houseGroup, templineStyle: { color: '#585858', radius: 500, fillOpacity: 0.4, dashArray: '5, 10', dashOffset: '20', }, hintlineStyle: { color: '#C14D4D', dashArray: '5, 10' }, pathOptions: { color: "#585858", fillColor: "#f2bd53", fillOpacity: 0.8 } }); } else if (Constructor.info.type === 'homestead') { map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, layerGroup: houseGroup, templineStyle: { color: '#585858', radius: 500, fillOpacity: 0.3, dashArray: '5, 10', dashOffset: '20', }, hintlineStyle: { color: '#C14D4D', dashArray: '5, 10' }, pathOptions: { color: "#f2bd53", fillColor: "#f2bd53", radius: 500, fillOpacity: 0.3, dashArray: '5, 10' } }); } }, async autoPoligon(IDs) { if (!IDs) return; const ids_list = IDs.replace(/\s+/g, "").split(','); Constructor.info.osm_id = ids_list; houseGroup.clearLayers(); homesteadGroup.clearLayers(); Constructor.info.points = []; Constructor.info.points_number = []; Constructor.info.geo = {} // 1006306041, 1006306065 for (let i = 0; i < ids_list.length; i++) { const element = await Constructor.osm.getOSM(Constructor.info.osm_id[i]); // Преобразуем координаты в LatLng const LatLngs = [[]]; element[0].forEach(feature => LatLngs[0].push({ lat: feature.lat, lng: feature.lng })); // Замыкаем полигон if (LatLngs[0][0] && LatLngs[0][0] !== LatLngs[0][LatLngs[0].length - 1]) { LatLngs[0].push(LatLngs[0][0]); } // Считаем центр const center = this.center(LatLngs); // Сохраняем в points / points_number Constructor.info.points.push(LatLngs); Constructor.info.points_number.push(center); // Создаем L.polygon const polyOptions = Constructor.info.type === 'homestead' ? { color: "#f2bd53", fillColor: "#f2bd53", fillOpacity: 0.4, dashArray: '5,10' } : { color: "#585858", fillColor: "#f2bd53", fillOpacity: 0.8 }; const house = L.polygon(LatLngs, polyOptions); // Добавляем в нужную группу if (Constructor.info.type === 'house') { houseGroup.addLayer(house); } else if (Constructor.info.type === 'homestead') { homesteadGroup.addLayer(house); } // Bind popup с кнопкой удаления house.bindPopup(` Координати: ${center.lat.toFixed(5)}, ${center.lng.toFixed(5)}
`); house.on('popupopen', (e) => { if (Constructor.homestead.building.editing) { house.closePopup(); return; } const btn = e.popup.getElement().querySelector('.map_dell'); if (btn) { btn.addEventListener('click', () => { Constructor.osm.delete(house); }); } }); } Constructor.osm.autoZoom(Constructor.info.points); }, center(geo) { // Получаем координаты полигона Leaflet let latlngs = geo[0]; // Преобразуем в формат GeoJSON для Turf const coordinates = latlngs.map(ll => [ll.lng, ll.lat]); const polygonGeoJSON = { type: "Feature", geometry: { type: "Polygon", coordinates: [coordinates] } }; // Находим центроид const centroid = turf.centroid(polygonGeoJSON); latlngs = { lat: centroid.geometry.coordinates[1], lng: centroid.geometry.coordinates[0] } return latlngs; }, autoZoom(polygons) { if (!polygons || !polygons.length) return; const allBounds = []; polygons.forEach(polygon => { const ring = polygon[0]; if (!ring || ring.length < 3) return; const coords = ring.map(p => [p.lng, p.lat]); if (coords[0][0] !== coords[coords.length - 1][0] || coords[0][1] !== coords[coords.length - 1][1]) { coords.push(coords[0]); } const polygonGeoJSON = turf.polygon([coords]); const bbox = turf.bbox(polygonGeoJSON); const bounds = L.latLngBounds( [bbox[1], bbox[0]], [bbox[3], bbox[2]] ); allBounds.push(bounds); }); if (!allBounds.length) return; // Если один полигон, просто fitBounds на него if (allBounds.length === 1) { map.fitBounds(allBounds[0]); } else { // Несколько полигонов → объединяем bounds let finalBounds = allBounds[0]; for (let i = 1; i < allBounds.length; i++) { finalBounds = finalBounds.extend(allBounds[i]); } map.fitBounds(finalBounds); } if (map.getZoom() > 18) map.setZoom(18); setTimeout(() => { Constructor.info.zoom = map.getZoom(); Constructor.info.geo = map.getCenter(); }, 200) }, delete(house) { // убрать слой с карты if (Constructor.info.type === 'house') { houseGroup.removeLayer(house); } else if (Constructor.info.type === 'homestead') { homesteadGroup.removeLayer(house); } // найти индекс полигона в points const index = Constructor.info.points.findIndex(p => p === house.getLatLngs()); if (index !== -1) { // удалить из points и points_number по индексу Constructor.info.points.splice(index, 1); Constructor.info.points_number.splice(index, 1); } Constructor.osm.autoZoom(Constructor.info.points); }, async getOSM(wayId) { const overpassUrl = `https://overpass-api.de/api/interpreter?data=[out:json];way(${wayId});(._;>;);out;`; return await fetch(overpassUrl) .then(response => response.json()) .then(data => { const nodes = new Map(); data.elements.forEach(el => { if (el.type === "node") { nodes.set(el.id, { lat: el.lat, lng: el.lon }); } }); const way = data.elements.find(el => el.type === "way"); if (way) { const coordinates = way.nodes.map(nodeId => nodes.get(nodeId)); return [coordinates]; } else { console.error("Way не найден!"); } }) .catch(error => console.error("Ошибка запроса:", error)); }, }, async save() { const part_3_button = document.getElementById('part-3-button'); console.log(Constructor.info); setLeafletCursor('pointer'); Constructor.homestead.building.editing = false; const uuid = localStorage.getItem('uuid'); const URL = `${CONFIG.api}constructor`; await fetch(URL, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": uuid }, body: JSON.stringify(Constructor.info) }) .then(response => { if (response.status == 200) { console.log({ 'setPack': 'ok' }); part_3_button.innerText = "Запис додано"; return response.json() } else { console.log('err'); part_3_button.innerText = "Помилка запису"; return } }) .then(data => { console.log(data); Territory.house.list = []; Territory.homestead.list = []; Router.navigate(`/territory/manager/${Constructor.info.type}/${data.id}`); setTimeout(() => { part_3_button.innerText = "Зберегти"; }, 3000); }) .catch(err => { console.log(err); part_3_button.innerText = "Помилка запису"; }) } }