UNPKG

galadrielmap_sk

Version:

a server-based chartplotter navigation software for pleasure crafts, motorhomes, and off-road cars. It's can be used on tablets and smartphones without install any app. Only browser need.

1,159 lines (1,072 loc) 195 kB
"use strict" /* Функции onBodyLoad() mapListPopulate() listPopulate(listObject,dirURI,chkCurrent=false,withExt=true,onComplete=undefined) Заполняет ul путей или маршрутов li с именами файлов ulDiff(listObjectIn,listObjectFrom) Удаляет из ul listObjectFrom li, присутствующие в listObjectIn getCookie(name) возвращает cookie с именем name, если есть, если нет, то undefined doSavePosition() Сохранение положения, списка показываемых карт, маршрутов и используемых параметров // Функции выбора - удаления карт selectMap(node) Выбор карты из списка имеющихся deSelectMap(node) Прекращение показа карты, и возврат её в список имеющихся. displayMap(mapname) Создаёт leaflet lauer с именем, содержащемся в mapname, и заносит его на карту removeMap(mapname) showMapsToggle() переключает показ всех или выбранных карт в списке карт // Функции выбора - удаления треков selectTrack() Выбор трека из списка имеющихся. deSelectTrack() Прекращение показа трека, и возврат его в список имеющихся. displayTrack() рисует трек с именем в trackNameNode displayRoute(routeNameNode) рисует маршрут или места с именем routeName updateCurrTrack() // Функции рисования маршрутов routeControlsDeSelect() pointsControlsDisable() pointsControlsEnable() getGPXicon(gpxtype) delShapes(realy,inLayer=null) Удаляет полилинии в состоянии редактирования, если realy = true tooggleEditRoute(e,flavor=null) createEditableMarker(Icon) doSaveMeasuredPaths() doRestoreMeasuredPaths() bindPopUptoEditable(layer) eraseEditable() startEditing() cancelEditing() resetRouteButtons() saveGPX(byLoadAction=undefined) Сохраняет на сервере маршрут из объекта currentRoute DOsaveGPX(byLoadAction=undefined) toGPX(geoJSON,createTrk) Create gpx route or track (createTrk==true) from geoJSON object // Кластеризация точек createSuperclaster(geoJSONpoints) removeFromSuperclaster(superclasterLayer,point) updateClasters() updClaster(e) realUpdClaster(layer) nextColor(color,step) // Показ координат центра и переход по введённым centerMarkPosition() centerMarkUpdate() centerMarkOn centerMarkOff flyByString(stringPos) Получает строку предположительно с координатами, и перемещает туда центр карты updGeocodeList(nominatim) doCopyToClipboard() Копирование в буфер обмена doCurrentTrackName(liID) doNotCurrentTrackName(liID) loggingWait() запускает/останавливает слежение за наличием пишущегося трека loggingRun() запускает/останавливает запись трека loggingCheck(logging=' ') // MOB MOBalarm() MOBtabHighLight(on=false) setMOBpopup(layer) createMOBpointMarker(mobMarkerJSON) MOBclose() realMOBclose() delMOBmarker() makeMOBmarkerCurrent(LMarker) clearCurrentStatus() удаляет признак "текущий маркер" у всех маркеров мультислоя mobMarker is_currentMOBmarkerSelf(marker) checkSelfMOBmarkerScount() mobMarkerDragendFunction(event) mobMarkerClickFunction(event) sendMOBtoServer() MOBtoGeoJSON(MOBdata) GeoJSONtoMOB(mobMarkerJSON,status,label) // Круги дистанции distCirclesUpdate() Устанавливает диаметр и подписи кругов дистанции distCirclesToggler() включает/выключает показ окружностей дистанции по переключателю в интерфейсе // Ветер windSwitchToggler() windSymbolUpdate() realWindSymbolUpdate(direction=0,speed=0) restoreDisplayedRoutes() chkDisplayedList(List,Displayed) Проверим соответствие списков hideControlsControl(hideControlPosition) Создаёт невидимый псевдо-control, по тапу по которому все указанные в controlsList control делаются невидимыми на экране hideControlEventListener(event) hideControlsToggler(target) // Функции маршрутной точки nextWPT() prevWPT() cancelFollowing() startFollowing() WPTbuttonsON() WPTbuttonsReady() WPTbuttonsOFF() WPTbuttonsNotReady() currentTrackUpdate() загружает трек, делает его показываемым и обновляет по мере записи loadScriptSync(scriptURL) Синхронная загрузка javascript bearing(latlng1, latlng2) tileNum2degree(zoom,xtile,ytile) Tile numbers to lon./lat. left top corner atou(b64) ASCII to Unicode (decode Base64 to original data) utoa(data) Unicode to ASCII (encode data to Base64) generateUUID() arrayHasOnly(array,value=null) getSelfPathC(path='') Получает с сервера path, синхронно realtime(dataUrl,fUpdate) Классы String.prototype.encodeHTML = function () L.Control.CopyToClipboard eachLayerRecursive() hasLayerRecursive(what) getLayerRecursive(what) storageHandler /////////////////////////// collisionDetector test /////////////////////////////// displayCollisionAreas() displayCollisionDetections() /////////////////////////// end collisionDetector test /////////////////////////////// */ /* // определение имени файла этого скрипта, например, чтобы знать пути на сервере const index = document.getElementsByTagName('script').length - 1; // это так, потому что эта часть сработает при загрузке скрипта, и он в этот момент - последний http://feather.elektrum.org/book/src.html var galadrielmapScript = scripts[index]; //console.log(galadrielmapScript); */ function onBodyLoad(){ listPopulate(routeList,routeDirURI,false,true,restoreDisplayedRoutes); // список маршрутов, асинхронно listPopulate(trackList,trackDirURI,true,false); // список путей, показывать текущий, асинхронно internalisationApply(); // подписи и заголовки, синхронно mapListPopulate(); // список карт, синхронно // Инициализируем список карт if(!showMapsList.length) showMapsToggle(true); // покажем в списке карт все карты, если нет избранных else showMapsToggle(); // покажем только избранные, поскольку изначально не показывается ничего // чего не сделаешь, если двойное нажатие не работает нигде, а на длительное в Google Chrome // и иже с ним навешана всякая фигня, и непросто навешана, а с запрещением всего остального function longressListener(e){ e.preventDefault(); //console.log(e.target); if(showMapsToggler.innerHTML == showMapsTogglerTXT[0]) return; // текущий режим - "избранные карты", в нём не работаем if(showMapsList.includes(e.target.id)){ // это избранная карта const n = showMapsList.indexOf(e.target.id); showMapsList.splice(n,1); // вырежем имя из массива e.target.classList.remove("showedMapName"); } else { showMapsList.push(e.target.id); e.target.classList.add("showedMapName"); } event.stopImmediatePropagation(); // прекратим всплытие и обломим все имеющиеся обработчики. Вдруг фигня, навешенная скотским Google, перестанет работать. //console.log('[longressListener] Список избранных карт:',showMapsList); } // end function long-pressListener let touchstartX, touchstartY; function handleSwipe(event){ let touchendX=event.changedTouches[0].screenX; let touchendY=event.changedTouches[0].screenY; //alert(`handleSwipe touchstartY=${touchstartY}, touchendY=${touchendY}`); if((touchendX > touchstartX+10) && (Math.abs(touchendY-touchstartY)<10)){ // вправо горизонтально //alert('handleSwipe горизонтальный жест'); longressListener(event); } } // end function handleSwipe() for(let mapLi of mapList.children){ // назначим обработчик длинного нажатия на каждое название карты, потому что его можно назначить только так mapLi.addEventListener('long-press', longressListener); // а также обработчики свайпа, ибо в мобильных Chrome вообще всё через жопу mapLi.addEventListener('touchstart',function(e){touchstartX=e.changedTouches[0].screenX; touchstartY=e.changedTouches[0].screenY;}); mapLi.addEventListener('touchend',handleSwipe); } } // end function onBodyLoad function mapListPopulate(){ // Карта должна быть, поэтому список карт -- синхронно. // Global: mapList - список карт в интерфейсе, ul let pluginMapList; const xhr = new XMLHttpRequest(); xhr.overrideMimeType("application/json"); xhr.open('GET', '/signalk/v1/api/resources/charts/', false); // Подготовим синхронный запрос xhr.send(); if (xhr.status == 200) { // Успешно try { pluginMapList = JSON.parse(xhr.responseText); // } catch(err) { // console.log('No any charts found. charts-plugin installed?'); } } //console.log(pluginMapList); const templateLi = mapList.querySelector('li[class="template"]'); // почему-то 'li[hidden]' не работает. //console.log(templateLi); for(let identifier in pluginMapList){ //console.log(identifier,pluginMapList[identifier]); let newLI = templateLi.cloneNode(true); newLI.classList.remove("template"); newLI.id = identifier; newLI.innerText = pluginMapList[identifier].name; newLI.hidden=false; //console.log(newLI); mapList.append(newLI); } } // end function mapListPopulate function listPopulate(listObject,dirURI,chkCurrent=false,withExt=true,onComplete=undefined){ // fetch(dirURI) // запросим список файлов route .then((response) => { return response.json(); }) .then(data => { //console.log('[listPopulate] data:',data); if(data){ if(chkCurrent) currentTrackName = data.currentTrackName.substring(0, data.currentTrackName.lastIndexOf('.')) || data.currentTrackName; // глобальная переменная if(data.filelist.length){ const templateLi = listObject.querySelector('li[class="template"]'); // почему-то 'li[hidden]' не работает. listObject.querySelectorAll('li').forEach(li => { // удалим из списка что там есть. delete использовать нельзя, потому что delete не уничтожает объекты, вопреки своему названию. if(li!=templateLi) { //console.log(li); li.remove(); li = null; } }); data.filelist.forEach(fileName => { if(!withExt) fileName = fileName.substring(0, fileName.lastIndexOf('.')) || fileName; let newLI = templateLi.cloneNode(true); newLI.classList.remove("template"); newLI.id = fileName; newLI.innerText = fileName; newLI.hidden=false; listObject.append(newLI); if(chkCurrent && fileName == currentTrackName) { // Сделаем текущим и запустим слежение doCurrentTrackName(fileName); // обязательно после append, ибо вне дерева элементы не ищутся. JavaScript -- коллекция нелепиц. } }); }; }; //console.log('[listPopulate] listObject:',listObject,'onComplete:',onComplete); if(onComplete) onComplete(); // здесь надо }).then(что?=>{if(onComplete) onComplete();}) ? }) .catch( (err) => { console.log(`Error get ${dirURI} files list:`,err.message); }); } // end function listPopulate function ulDiff(listObjectIn,listObjectFrom){ /* Удаляет из ul listObjectFrom li, присутствующие в listObjectIn Может быть использована в listPopulate в качестве функции onComplete для удаления из созданного списка имеющихся списка показываемых. Например, из routeList (listObjectFrom) удалить routeDisplayed (listObjectIn) */ const listObjectFromLiS = listObjectFrom.querySelectorAll('li'); // список всех li в listObjectFrom listObjectIn.querySelectorAll('li').forEach(function (displayedLi){ // для каждого элемента списка всех li из listObjectIn //console.log('displayedLi:',displayedLi.id); for(const li of listObjectFromLiS){ // прокрутим список всех li в listObjectFrom //console.log('\trouteList li',li.id); if(displayedLi.id==li.id){ // если элемент из списка listObjectFrom есть в списке элементов listObjectIn li.remove(); // method removes the element from the DOM. Объект остаётся в коллекции routeListLi? Похоже, да, хотя не должен? break; }; }; }); }; // end function ulDiff function getCookie(name) { // возвращает cookie с именем name, если есть, если нет, то undefined name=name.trim(); var matches = document.cookie.match(new RegExp( "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" ) ); return matches ? decodeURIComponent(matches[1]) : null; }; // end function getCookie function doSavePosition(){ /* Сохранение переменных. Обычно - отдельно по интервалу. global map, mapDisplayed, document, currTrackSwitch, vesselSelf */ let toSave = {'startCenter':map.getCenter()}; toSave['startZoom'] = map.getZoom(); // Сохранение показываемых карт let openedNames = []; for (let i = 0; i < mapDisplayed.children.length; i++) { // для каждого потомка списка mapDisplayed //console.log('mapDisplayed li',mapDisplayed.children[i]); openedNames[i] = mapDisplayed.children[i].id; // } toSave['layers'] = openedNames; // Сохранение показываемых маршрутов openedNames = []; for (let i = 0; i < routeDisplayed.children.length; i++) { // для каждого потомка списка mapDisplayed openedNames[i] = routeDisplayed.children[i].innerHTML; // } toSave['showRoutes'] = openedNames; // Сохранение переключателей и параметров toSave['vesselSelf'] = vesselSelf; toSave['currTrackSwitch'] = currTrackSwitch.checked; toSave['loggingSwitch'] = loggingSwitch.checked; toSave['SelectedRoutesSwitch'] = SelectedRoutesSwitch.checked; toSave['minWATCHinterval'] = minWATCHinterval; toSave['showMapsList'] = showMapsList; storageHandler.save(toSave); }; // end function doSavePosition // Функции выбора - удаления карт function selectMap(node) { // Выбор карты из списка имеющихся. Получим объект //console.log(node); node.hidden = false; mapDisplayed.insertBefore(node,mapDisplayed.firstChild); // из списка доступных в список показываемых (объект, на котором событие, добавим в конец потомков mapDisplayed) node.onclick = function(event){deSelectMap(event.currentTarget);}; displayMap(node.id); // SignalK } function deSelectMap(node) { // Прекращение показа карты, и возврат её в список имеющихся. Получим объект var li = null; for (var i = 0; i < mapList.children.length; i++) { // для каждого потомка списка mapList li = mapList.children[i]; // взять этого потомка var childTitle = li.innerHTML; if (childTitle > node.innerHTML) { // если наименование потомка дальше по алфавиту, чем наименование того, на что кликнули break; } li = null; } mapList.insertBefore(node,li); // перенесём перед тем, на котором обломался цикл, или перед концом node.onclick = function(event){selectMap(event.currentTarget);}; if(showMapsToggler.innerHTML == showMapsTogglerTXT[0]){ // текущий режим - "только избранные" if(!showMapsList.includes(node.id)) node.hidden = true; } else { // текущий режим - "все карты" if(showMapsList.includes(node.id)) node.classList.add("showedMapName"); } removeMap(node.id); // SignalK } function displayMap(mapname,mapParm={}) { // mapParm Нет в этой версии! if(savedLayers[mapname] == null) { const layer = realDisplayMap(mapname,mapParm); if(layer == null) return; if(typeof layer.options.javascriptOpen === 'function') { layer.options.javascriptOpen(layer); }; if(savedLayers[mapname]) savedLayers[mapname].remove(); savedLayers[mapname] = layer; //console.log('[displayMap] mapname=',mapname,'savedLayers[mapname]:',savedLayers[mapname]); savedLayers[mapname].addTo(map); } else { // такая карта уже есть, просто покажем if(typeof savedLayers[mapname].options.javascriptOpen === 'function') { savedLayers[mapname].options.javascriptOpen(savedLayers[mapname]); }; savedLayers[mapname].addTo(map); }; //console.log('[displayMap] mapname=',mapname,'mapParm:',savedLayers[mapname].options.mapParm); //autoMapUpdate(savedLayers[mapname],true) // Нет в этой версии! Включить автоматическое обновление // //if(map.hasLayer(tileGrid)) displayMapBounds(); // Нет в этой версии! перерисуем границы карт, если показываем сетку }; // end function displayMap function contourLines(mapname,mapParm={}){ /**/ function realContourLines(maplibreMap){ //console.log('[realContourLines] maplibreMap:',maplibreMap); if(maplibreMap.isStyleLoaded()){ //console.log('[realContourLines] maplibreMap style:',maplibreMap.getStyle()); const style = maplibreMap.getStyle(); let layer; for(layer of style.layers){ if(layer.type == 'hillshade'){ //console.log('[realContourLines] слой',layer,'является тонировкой рельефа'); //console.log('[realContourLines] слой найден в maplibreMap:',maplibreMap); break; // там только один слой hillshade? Ну, по логике вещей... }; }; if(layer.type != 'hillshade') return; // слоя hillshade в карте maplibre нет. //console.log('[realContourLines] Загружена ли maplibre-contour?:',mapboxCountourscript); if(!mapboxCountourscript){ // библиотека maplibre-contour ещё не загружена if(mapboxCountourscript=loadScriptSync("maplibre-contour/dist/index.js")) console.log("[realDisplayMap] maplibre-contour is loaded"); else return; // не удалось загрузить библиотеку maplibre-contour }; // У этих придурков "The URL must be absolute, containing the scheme, authority and path components." https://maplibre.org/maplibre-style-spec/glyphs/ //console.log(window.location.origin,window.location.pathname); // Ещё у этих придурков не работают функции getGlyphs() и setGlyphs(). // Однако, версия @5.19.0 (последняя), имеет встроенный шрифт, и может обходиться без glyphs. // Но она на 300K больше. // Но @5.19.0 не падает на .getStyle() после замены glyphs в setStyle if(!style.glyphs) { //console.log('[realContourLines] glyphs нет, добавляем'); //maplibreMap.setGlyphs(`${window.location.origin}${window.location.pathname}styles/fonts/{fontstack}/{range}.pbf`); style.glyphs = `${window.location.origin}${window.location.pathname}styles/fonts/{fontstack}/{range}.pbf`; maplibreMap.setStyle(style); // стиль будет полностью заменён?, потому что изменён glyphs setTimeout(realContourLines,10,maplibreMap); // запустим ожидание обновления стиля. Строго говоря, даже повторная проверка на наличие слоя hillshade будет уместна - вдруг его кто-то удалит? return; // и на этом всё, ибо считаем, что без glyphs упс. }; //console.log('[realContourLines] Стиль есть, можно рисовать карту',maplibreMap.getStyle()) // Итак, glyphs есть. Пора рисовать горизонтали. //console.log('Объект sources для найденного слоя типа hillshade:',style.sources[layer.source]); //console.log('Объект sources для найденного слоя типа hillshade с ID:',layer.id,maplibreMap.getSource(layer.id)); // Стиль, сцуко, в этот момент ещё не загружен, и их горбатая функция возвращает undefined const url = style.sources[layer.source].tiles[0]; const maxzoom = style.sources[layer.source].maxzoom || 17; //console.log('url=',url,'maxzoom=',maxzoom); //let DEMencoding = mapParm.clientData.DEMencoding; let DEMencoding; // В SignalK нет способа передать... if(DEMencoding != 'mapbox') DEMencoding = 'terrarium'; let demSource = new mlcontour.DemSource({ "url" : url, // А какого хрена кодировка DEM не указывается в стиле, если там есть "type": "hillshade"? // а потому что для hillshade не нужна абсолютная высота, а относительные одинаковые в обоих кодировках. // "mapbox" or "terrarium" default="terrarium" "encoding" : DEMencoding, "maxzoom" : maxzoom, "worker" : true, // offload isoline computation to a web worker to reduce jank //cacheSize: 100, // number of most-recent tiles to cache timeoutMs: 10_000, // timeout on fetch requests }); demSource.setupMaplibre(maplibregl); // Then configure a new contour source and add it to your map: /*/ The demSource.contourProtocolUrl thresholds parameter in MapLibre/maplibre-contour defines elevation intervals for minor and major contour lines, mapped by zoom level (zoom: [minor, major]). It controls which contour lines are displayed (e.g., every 50m/200m or 20m/100m) to reduce clutter at different map scales. Common Threshold Configurations (Zoom: [Minor, Major]): Detailed (Zoom 14+): 14: [50, 200] or 14: [20, 100] for high-resolution terrain. Mid-range (Zoom 11-13): 11: [200, 1000], 12: [100, 500]. Example Usage: thresholds: { 11: [200, 1000], 12: [100, 500], 14: [50, 200], 15: [20, 100] }. Key Details: Units: Values are generally in meters unless a multiplier is applied (e.g., 3.28084 for feet). Structure: Defines when to show lighter, thin lines (minor) and darker, thick lines (major). Alternative Inline Format: In URLs, these can be specified as 11*250*1000 (Zoom * Minor * Major). /*/ maplibreMap.addSource("contourSource", { "type": "vector", "tiles": [ demSource.contourProtocolUrl({ // convert meters to feet, default=1 for meters //"multiplier": 3.28084, "thresholds": { // zoom: [minor, major] 11: [100, 500], 12: [50, 500], 13: [10, 200], 14: [5, 100], 15: [2, 100] }, // optional, override vector tile parameters: "contourLayer": "contours", "elevationKey": "ele", // имя свойства mlcontour в maplibre style "levelKey": "level", // "major" contours have level=1, "minor" have level=0 "overzoom": 1, //"extent": 4096, //"buffer": 1, }) ], "maxzoom": maxzoom }); // Then add contour line and label layers: maplibreMap.addLayer({ "id": "contourLines", // Подразумевается, что там высот < 0 нет, хотя никто не запрещает. "type": "line", "source": "contourSource", "source-layer": "contours", "filter": ["!=", ["get", "ele"], 0], "paint": { //"line-color": "rgba(0,0,0, 30%)", "line-color": "rgba(230,135,30, 75%)", //"line-color": "red", // level = highest index in thresholds array the elevation is a multiple of "line-width": ["match", ["get", "level"], 1, 1, 0.5], }, }); maplibreMap.addLayer({ "id": "contourLabels", "type": "symbol", "source": "contourSource", "source-layer": "contours", "filter": ["all", ["!=", ["get", "ele"], 0], [">", ["get", "level"], 0] ], "layout": { "symbol-placement": "line", "text-size": 10, //"text-field": ["concat", ["number-format", ["get", "ele"], {}], "'"], "text-field": ["number-format", ["get", "ele"], {}], "text-font": ["Noto Sans Bold"], }, "paint": { "text-halo-color": "white", "text-halo-width": 1, }, }); } else { //console.log('[realContourLines] Стилей ещё нет, ждём'); setTimeout(realContourLines,100,maplibreMap); }; }; // end function realContourLines function waitMapLibreMap(Llayer){ // У этих придурков объект _glMap появляется в объекте l.maplibreGL только после .addTo(map)? Не, когда всё загрузится. Или на следующий оборот? if(typeof Llayer.getMaplibreMap === 'function'){ //console.log('[waitMapLibreMap] Это Leaflet слой maplibre'); const Mmap = Llayer.getMaplibreMap(); if(Mmap === undefined) setTimeout(waitMapLibreMap,100,Llayer); // но карты maplibre там нет else realContourLines(Mmap); }; }; // end function waitMapLibreMap let mapObj; if(typeof mapname === 'string') mapObj = savedLayers[mapname]; else mapObj = mapname; if(mapObj instanceof L.LayerGroup) { // это layerGroup for(let layer of mapObj.getLayers()){ waitMapLibreMap(layer); }; } else waitMapLibreMap(mapObj); // }; // end function contourLines function realDisplayMap(mapname,mapParm={}) { /* Создаёт leaflet lauer с именем, содержащемся в mapParm, и заносит его на карту Для SignalK mapname -- это identifier в смысле chart-plugin. Делает запрос к SignalK для получения параметров карты Если в имени карты есть EPSG3395 - делает слой в проекции с пересчётом с помощью L.tileLayer.Mercator */ mapname=mapname.trim(mapname); // Всегда будем спрашивать параметры карты const xhr = new XMLHttpRequest(); xhr.open('GET', '/signalk/v1/api/resources/charts/'+mapname, false); // Подготовим синхронный запрос xhr.send(); if (xhr.status == 200) { // Успешно let skMapParm; try { skMapParm = JSON.parse(xhr.responseText); // параметры карты } catch(err) { // console.log('[realDisplayMap] Get chart '+mapname+' metainfo error:',err); return; }; //console.log('[displayMap] skMapParm:',skMapParm); mapParm.identifier=mapname; // Для SignalK mapname -- это identifier в смысле chart-plugin. mapParm.name=skMapParm.name; mapParm.ext=skMapParm.format; mapParm.minZoom=skMapParm.minzoom || 0; mapParm.maxZoom=skMapParm.maxzoom || 16; mapParm.mapTiles=skMapParm.tilemapUrl; //mapParm.data=skMapParm.data; // Этого там нет, и неизвестно, будет ли mapParm.vectorTileStyleURL=skMapParm.style; // имя файла стиля mapbox, для векторных тайлов. Этого там нет, и неизвестно, будет ли if(skMapParm.bounds && ((skMapParm.bounds[0]!=-180)&&(skMapParm.bounds[1]!=-90)&&(skMapParm.bounds[2]!=180)&&(skMapParm.bounds[3]!=90))){ // In format: {"leftTop":{"lat":lat,"lng":lng},"rightBottom":{"lat":lat,"lng":lng}} mapParm.bounds = {"leftTop":{"lat":skMapParm.bounds[1],"lng":skMapParm.bounds[0]},"rightBottom":{"lat":skMapParm.bounds[3],"lng":skMapParm.bounds[2]}}; }; mapParm.skMapParm = skMapParm; // у них там тривиальные наименования, и у меня тоже. Но смысл-то разный. } else { console.log('[realDisplayMap] xhr request ERROR:',xhr.status); return; }; // используются ли векторные тайлы const isVector = (mapParm.ext=='pbf') || (mapParm.ext=='mvt'); if(!mapParm.mapTiles && !isVector){ // не указано, как получать тайлы. Однако, если mvt - то там это указано в стиле? return; }; let minNativeZoom,maxNativeZoom; if(mapParm.minZoom>9){ minNativeZoom = mapParm.minZoom; mapParm.minZoom = 9; }; if(mapParm.maxZoom<16){ maxNativeZoom = mapParm.maxZoom; mapParm.maxZoom += 2; }; mapParm.minNativeZoom = minNativeZoom; mapParm.maxNativeZoom = maxNativeZoom; let layerParm; if(isVector){ layerParm = { // здесь всё в стиле, и, например, указание minzoom даёт весёлые визуальные эффекты "style": mapParm.vectorTileStyleURL, }; } else { layerParm = { "minZoom":mapParm.minZoom, "maxZoom":mapParm.maxZoom, "minNativeZoom":mapParm.minNativeZoom, "maxNativeZoom":mapParm.maxNativeZoom }; if(mapParm.bounds && (JSON.stringify(mapParm.bounds)!='[]')) { //console.log('[realDisplayMap] mapParm.bounds:',JSON.stringify(mapParm.bounds)); let leftTop = {}; leftTop.lng = mapParm.bounds.leftTop.lng; // а иначе оно ссылка, б...! leftTop.lat = mapParm.bounds.leftTop.lat; let rightBottom = {}; rightBottom.lng = mapParm.bounds.rightBottom.lng; rightBottom.lat = mapParm.bounds.rightBottom.lat; if(mapParm.bounds.leftTop.lng>0 && mapParm.bounds.rightBottom.lng<=0){ // граница переходит антимередиан // Не вполне понятна глубинная суть этого деяния, но факт в том, что если не вычитать/прибавлять, // то не видны либо левые, либо правые части. А если ничего не делать - то не видно ничего. leftTop.lng -= 360; rightBottom.lng += 360; }; layerParm.bounds = L.latLngBounds(leftTop,rightBottom); }; }; let mapTilesURIthis; if(mapParm.r) mapTilesURIthis = mapParm.mapTiles.replace('{map}',mapParm.r.trim()); // нужно, как минимум, для COVER. else mapTilesURIthis = mapParm.mapTiles.replace('{map}',mapname); if(mapParm.requestOptions) mapTilesURIthis = mapTilesURIthis.replace('{options}',JSON.stringify(mapParm.requestOptions)); else mapTilesURIthis = mapTilesURIthis.replace('{options}',''); if(mapParm.ext) mapTilesURIthis = mapTilesURIthis.replace('{ext}',mapParm.ext); //console.log('[realDisplayMap] mapTilesURIthis:',mapTilesURIthis); //console.log('[realDisplayMap] layerParm:',layerParm); let layer; if((mapParm.epsg && String(mapParm.epsg).indexOf('3395')!=-1)||(mapname.indexOf('EPSG3395')!=-1)) { layer = L.tileLayer.Mercator(mapTilesURIthis, layerParm); } else if(isVector) { // векторные тайлы if(typeof L.maplibreGL === 'undefined'){ // если ещё не загружено //if(typeof L.mapboxGL === 'undefined'){ // если ещё не загружено let link = document.createElement('link'); link.type = 'text/css'; link.href = 'style.css'; link.rel = 'maplibre-gl/dist/mapbox-gl.css'; //link.rel = 'mapbox-gl-js/dist/mapbox-gl.css'; document.head.appendChild(link); if(!(mapboxGLscript=loadScriptSync("maplibre-gl/dist/maplibre-gl.js"))) return; // Нахрена присваивать глобальной переменной, которая нигде не используется -- неясно, но без этого возникает ошибка при закрытии карты. if(!(mapboxLeafletscript=loadScriptSync("maplibre-gl-leaflet/leaflet-maplibre-gl.js"))) return; //if(!(mapboxGLscript=loadScriptSync("mapbox-gl-js/dist/mapbox-gl.js"))) return; //if(!(mapboxLeafletscript=loadScriptSync("mapbox-gl-leaflet/leaflet-mapbox-gl.js"))) return; console.log("[realDisplayMap] gl & gl-leaflet is loaded"); }; layer = L.maplibreGL(layerParm); //layer = L.mapboxGL(layerParm); contourLines(layer,mapParm); } else if(mapParm.skMapParm.type == 'WMS'){ if(mapParm.skMapParm.chartLayers) layerParm.layers = mapParm.skMapParm.chartLayers.join(); // параметр карты wms layerParm.style = mapParm.vectorTileStyleURL || '', layer = L.tileLayer.wms(mapTilesURIthis, layerParm); } else { layer = L.tileLayer(mapTilesURIthis, layerParm); }; layer.options.mapname = mapname; //console.log('[realDisplayMap] mapParm:',mapParm); layer.options.mapParm = mapParm; // установим текущий масштаб в пределах возможного для указанной карты // Это всё должно быть в displayMap, а не здесь? if(!layer.options.zoom) { // т.е., не устанавливали уже масштаб по какому-то слою let currZoom = map.getZoom(); //console.log('[realDisplayMap] currZoom=',currZoom,'mapParm.maxZoom=',mapParm.maxZoom); if(mapParm.maxNativeZoom < currZoom) { map.setZoom(mapParm.maxZoom); // установим масштаб в видимость этого слоя layer.options.zoom = currZoom; // запомним, какой был, чтобы потом восстановить } else if(mapParm.minNativeZoom > currZoom) { map.setZoom(mapParm.minZoom); layer.options.zoom = currZoom; } else layer.options.zoom = false; }; if(layer.options.mapParm.bounds) { // карте указаны рамки в описании. LayerGroup не имеет свойства bounds // Если карта составная, то в каждой составляющей - свои границы. Но к этому моменту те границы // уже сработали?, и здесь будет позиционировано в пределах границ объемлющей карты. const bondsPoint = isPointInBounds(map.getCenter(),layer.options.mapParm.bounds); if(bondsPoint !== true) map.setView(bondsPoint); }; // javascript в загружаемом источнике на закрытие карты // window.eval выполняет eval в глобальном контексте, в результате можно в eval объявить // глобальные функции и переменные if(mapParm.clientData && (typeof mapParm.clientData.javascriptClose === "string")) { layer.options.javascriptClose = window.eval(mapParm.clientData.javascriptClose); // eval должен возвращать функцию }; // javascript в загружаемом источнике на открытие карты if(mapParm.clientData && (typeof mapParm.clientData.javascriptOpen === "string")) { layer.options.javascriptOpen = window.eval(mapParm.clientData.javascriptOpen); // eval должен возвращать функцию }; //console.log('[realDisplayMap] layer:',layer); return layer; } // end function realDisplayMap function isPointInBounds(point,bounds){ /* Находится ли точка в границах, а если нет - где ближайшая граница */ const nearestPoint = {"lat":null,"lng":null}; // Широта if(point.lat > bounds.leftTop.lat){ // Точка севернее рамки, т.е., не в границах. // Очевидно, ближайшая широтная граница рамки - северная nearestPoint.lat = bounds.leftTop.lat; } else if(point.lat < bounds.rightBottom.lat){ // Точка южнее рамки, т.е., не в границах. // Очевидно, ближайшая широтная граница рамки - южная nearestPoint.lat = bounds.rightBottom.lat; } // иначе - точка по широте в пределах рамки // Долгота const dLon = Math.abs(bounds.leftTop.lng - bounds.rightBottom.lng); if(dLon < 180) { //console.log("рамка не пересекает антимеридиан"); if(point.lng < bounds.leftTop.lng){ //console.log("Точка западнее левой рамки и восточнее антимередиана, т.е., не в границах"); const dL = bounds.leftTop.lng-point.lng; // градусов между долготой точки и долготой левой границы рамки const dR = 360-bounds.rightBottom.lng-point.lng; // оставшаяся часть круга, градусов между долготой точки и долготой правой границы рамки if(dL<dR){ // до западной границы рамки ближе, чем до восточной nearestPoint.lng = bounds.leftTop.lng; } else{ nearestPoint.lng = bounds.rightBottom.lng; }; } else if(point.lng > bounds.rightBottom.lng){ //console.log("Точка восточнее правой рамки и западнее антимередиана, т.е., не в границах"); const dR = point.lng - bounds.rightBottom.lng; // градусов между долготой точки и долготой правой границы рамки const dL = 360-point.lng-bounds.leftTop.lng; // оставшаяся часть круга, градусов между долготой точки и долготой левой границы рамки if(dL<dR){ // до западной границы рамки ближе, чем до восточной nearestPoint.lng = bounds.leftTop.lng; } else{ nearestPoint.lng = bounds.rightBottom.lng; }; }; } else { //console.log("рамка пересекает антимередиан"); if((point.lng < bounds.leftTop.lng) && (point.lng > bounds.rightBottom.lng)){ //console.log("Точка западнее левой рамки и восточнее правой, т.е., не в границах"); const dL = Math.abs(bounds.leftTop.lng-point.lng); // градусов между долготой точки и долготой левой границы рамки let dR = Math.abs(bounds.rightBottom.lng-point.lng); // оставшаяся часть круга, градусов между долготой точки и долготой правой границы рамки //console.log('от левой рамки до точки:',dL,'от правой рамки до точки:',dR); if(dL<dR){ // до левой границы рамки ближе, чем до правой nearestPoint.lng = bounds.leftTop.lng; } else{ nearestPoint.lng = bounds.rightBottom.lng; }; }; }; if((nearestPoint.lat===null)&&(nearestPoint.lng===null)) return true; // точка внутри рамки else if(nearestPoint.lat===null){ //console.log(" точка в пределах широты, но не попадает по долготе"); nearestPoint.lat = point.lat; } else if(nearestPoint.lng===null){ //console.log("точка в пределах долготы, но не попадает по широте"); nearestPoint.lng = point.lng; } // иначе - точка вне рамки return nearestPoint; }; // end function isPointInBounds function removeMap(mapname) { // Для SignalK mapname -- это identifier в смысле chart-plugin. mapname=mapname.trim(); if(!savedLayers[mapname]) return; // например, в списке есть трек, но gpx был кривой, и слой не был создан if(savedLayers[mapname].options.javascriptClose) eval(savedLayers[mapname].options.javascriptClose); if(savedLayers[mapname].options.zoom) { map.setZoom(savedLayers[mapname].options.zoom); // вернём масштаб как было savedLayers[mapname].options.zoom = false; } savedLayers[mapname].remove(); // удалим слой с карты //savedLayers[mapname] = null; // удалим сам слой. Но это не надо, ибо включение/выключение отображения слоёв должно быть быстро, и обычно их не надо повторно получать с сервера } // end function removeMap function showMapsToggle(all=false){ /* переключает показ всех или выбранных карт в списке карт */ //console.log('[showMapsToggle] showMapsList:',showMapsList); if(all || showMapsToggler.innerHTML == showMapsTogglerTXT[0]){ // текущий режим - "избранные карты" (на кнопке надпись: "все карты") for(let mapLi of mapList.children){ //console.log('покажем все карты',mapLi.id); mapLi.hidden = false; // покажем все карты if(showMapsList.includes(mapLi.id)){ // избранная карта mapLi.classList.add("showedMapName"); } } showMapsToggler.innerHTML = showMapsTogglerTXT[1]; // сменим режим на "все карты" } else { // текущий режим - "все карты" - покажем только избранные for(let mapLi of mapList.children){ //console.log('покажем только избранные',mapLi.id); mapLi.hidden = false; // при старте они все скрытые if(!showMapsList.includes(mapLi.id)){ // карта не в списке избранных mapLi.hidden = true; // не покажем карту } mapLi.classList.remove("showedMapName"); } showMapsToggler.innerHTML = showMapsTogglerTXT[0]; // сменим режим на "избранные карты" } } // end function showMapsToggle // Функции выбора - удаления треков function selectTrack(node,trackList,trackDisplayed,displayTrack) { /* Выбор трека из списка имеющихся. node - объект li, элемент списка имеющихся, который выбрали trackList - объект ul, список имеющихся trackDisplayed - объект ul, список выбранных displayTrack - функция показывания того, что соответствует выбранному элементу global deSelectTrack() currentTrackShowedFlag */ //console.log(trackDisplayed.firstChild); trackDisplayed.insertBefore(node,trackDisplayed.firstChild); // из списка доступных в список показываемых (объект, на котором событие, добавим в конец потомков mapDisplayed) node.onclick = function(event){deSelectTrack(event.currentTarget,trackList,trackDisplayed,displayTrack);}; if(node.title.toLowerCase().indexOf("current")!= -1) { // текущий трек currentTrackShowedFlag = 'loading'; // укажем, что трек сейчас загружается startCurrentTrackUpdateProcess(); // запустим обновление трека } //console.log('[selectTrack] node.title=',node.title,'currentTrackShowedFlag=',currentTrackShowedFlag); displayTrack(node); // создадим трек } // end function selectTrack function deSelectTrack(node,trackList,trackDisplayed,displayTrack) { /* Прекращение показа трека, и возврат его в список имеющихся. Получим объект node - объект li, элемент списка показываемых, который выбрали для непоказывания trackList - объект ul, список имеющихся, куда надо вернуть node global selectTrack() */ if(node.title.toLowerCase().indexOf("current")!= -1) { // текущий трек if(!currTrackSwitch.checked){ // Текущий трек не всегда показывается if(currentTrackUpdateProcess) { clearInterval(currentTrackUpdateProcess); currentTrackUpdateProcess = null; } // здесь не надо убивать слежение, потому что оно используется для получения // результатов асинхронного сервера //if(currentWaitTrackUpdateProcess) { // clearInterval(currentWaitTrackUpdateProcess); // // currentWaitTrackUpdateProcess = null; //} } }; var li = null; for (var i = 0; i < trackList.children.length; i++) { // для каждого потомка списка trackList li = trackList.children[i]; // взять этого потомка var childTitle = li.innerHTML; if (childTitle > node.innerHTML) { // если наименование потомка дальше по алфавиту, чем наименование того, на что кликнули break; } li = null; } trackList.insertBefore(node,li); // перенесём перед тем, на котором обломался цикл, или перед концом //console.log(node); node.onclick = function(event){selectTrack(event.currentTarget,trackList,trackDisplayed,displayTrack);}; removeMap(node.innerHTML); } function displayTrack(trackNameNode) { /* рисует трек с именем в trackNameNode global trackDirURI, window, currentTrackName */ var trackName = trackNameNode.innerText.trim(); //console.log('[displayTrack] trackName=',trackName,'currentTrackName=',currentTrackName,'savedLayers[trackName]',savedLayers[trackName]); if( savedLayers[trackName]) { //console.log('[displayTrack] Рисуем на карте из кеша trackName=',trackName); savedLayers[trackName].addTo(map); // нарисуем его на карте. Текущий трек всегда перезагружаем в updateCurrTrack } else { // просто спрашиваем у сервера файл, там не ответчик var options = {featureNameNode : trackNameNode}; var xhr = new XMLHttpRequest(); //console.log('[displayTrack] Загружаем новый файл trackName=',trackDirURI+'/'+trackName+'.gpx'); xhr.open('GET', encodeURI(trackDirURI+'/'+trackName+'.gpx'), true); // Подготовим асинхронный запрос xhr.overrideMimeType( "application/gpx+xml; charset=UTF-8" ); // тупые уроды из Mozilla считают, что если не указан mime type ответа -- то он text/xml. Файлы они, очевидно, не скачивают. xhr.send(); xhr.onreadystatechange = function() { // trackName - внешняя if (this.readyState != 4) return; // запрос ещё не завершился, покинем функцию if (this.status != 200) { // запрос завершлся, но неудачно console.log('[displayTrack] To request file '+trackDirURI+'/'+trackName+' server return '+this.status); if(trackNameNode.title.toLowerCase().indexOf("current")!= -1) { // текущий трек currentTrackShowedFlag = 'error'; // укажем, что с треком что-то не то } return; // что-то не то с сервером } // В злопаршивом Javascript символ /00 пробельным не является //console.log('|'+this.responseText.slice(-10)+'|'); let str = this.responseText.replace(/\0+|\0+/g, '').trim().slice(-12); //console.log('[displayTrack] |'+str+'|'); if(!str) return; if(str.indexOf('</gpx>') == -1) { // может получиться кривой gpx -- по разным причинам //console.log('кривой gpx',str); // незавершённый gpx - дополним до конца. Поэтому скачиваем сами, а не omnivore let responseText = this.responseText.replace(/\0+|\0+/g, '').trim(); // потому что this.responseText не строка, а getter-only property, хрен его знает, что это значит и зачем. if(str.endsWith('</trkpt>')) responseText += '\n </trkseg>\n </trk>\n</gpx>'; // точку оно всегда? успевает записать else if(str.endsWith('</trkseg>')) responseText += '\n </trk>\n</gpx>'; else responseText += '\n</gpx>'; // на самом деле, здесь </metadata>, т.е., gpxlogger запустился, но ничего не пишет: нет gpsd, нет спутников, нет связи... savedLayers[trackName] = omnivore.gpx.parse(responseText,options); } else { savedLayers[trackName] = omnivore.gpx.parse(this.responseText,options); // responseXML иногда почему-то кривой } //console.log(savedLayers[trackName]); savedLayers[trackName].addTo(map); // нарисуем его на карте } } } // end function displayTrack function displayRoute(routeNameNode,changed=false) { /* рисует маршрут или места с именем routeName global routeDirURI map window */ var routeName = routeNameNode.innerText.trim(); var options = {featureNameNode : routeNameNode}; if(!changed && savedLayers[routeName]) { savedLayers[routeName].addTo(map); // нарисуем его на карте. } else { // удалим с карты объект с этим именем, потому что дальше с этим именем // будет другой объект if(savedLayers[routeName]) savedLayers[routeName].remove(); var routeType = routeName.slice((routeName.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase(); // https://www.jstips.co/en/javascript/get-file-extension/ потому что там нет естественного пути //console.log(routeType); switch(routeType) { case 'gpx': savedLayers[routeName] = omnivore.gpx(routeDirURI+'/'+routeName,options); if(! updateRoutesInterval) { // Запуск динамического обновления показываемых маршрутов, // если ещё не запущен и есть адрес обновлялки if(updateRouteServerURI) updateRoutesInterval = setInterval(realtime,3000,updateRouteServerURI,routeUpdate); }; break; case 'kml': savedLayers[routeName] = omnivore.kml(routeDirURI+'/'+routeName,options); break; case 'csv': savedLayers[routeName] = omnivore.csv(routeDirURI+'/'+routeName,options); break; } //console.log('[displayRoute] routeName=',routeName,'savedLayers[routeName]:',savedLayers[routeName]); if( savedLayers[routeName]) { if(!('properties' in savedLayers[routeName])) savedLayers[routeName].properties = {}; savedLayers[routeName].properties.fileName = routeName; // имя файла. А нафига? А чтобы потом понять, что объект загружен из файла savedLayers[routeName].addTo(map); } } } // end function displayRoute function updateCurrTrack() { // Получим GeoJSON - ломаную из скольких-то последних путевых точек, или false, если с последнего // обращения нет новых точек // в формате GeoJSON //console.log(currentTrackServerURI,currentTrackName); var xhr = new XMLHttpRequest(); xhr.open('GET', encodeURI(currentTrackServerURI+'/'+currentTrackName), true); // Подготовим асинхронный запрос xhr.send(); xhr.onreadystatechange = function() { // if (this.readyState != 4) return; // запрос ещё не завершился, покинем функцию if (this.status != 200) { // запрос завершлся, но неудачно //console.log('Server return '+this.status+'\ncurrentTrackServerURI='+currentTrackServerURI+'\ncurrTrackName='+currentTrackName+'\n\n'); //console.log('To [updateCurrTrack] server return '+this.status+' instead '+currentTrackName+' last segment.'); if(typeof loggingIndicator != 'undefined'){ // лампочка в интерфейсе loggingIndicator.style.color='red'; loggingIndicator.innerText='\u2B24'; } return; // что-то не то с сервером } //console.log('updateCurrTrack responseText=',this.responseText); let resp = {}; try { resp = JSON.parse(this.responseText); } catch(err) { if(this.responseText.trim()) console.log('Bad data to update current track:'+this.responseText+';',err.message) } //console.log('[updateCurrTrack] resp:',resp); if(resp.logging){ // лог пишется if(typeof loggingIndicator != 'undefined'){ // лампочка в интерфейсе. Вообще-то, в этом варианте софта эта лампочка всегда есть. loggingIndicator.style.color='green'; loggingIndicator.innerText='\u2B24'; } if(resp.pt) { // есть данные if(savedLayers[currentTrackName]) { // может не быть, если, например, показ треков выключили, но выполнение currentTrackUpdate уже запланировано //if(savedLayers[currentTrackName].getLayers()) { // это LayerGroup if(savedLayers[currentTrackName] instanceof L.LayerGroup) { // это LayerGroup savedLayers[currentTrackName].getLayers()[0].addData(resp.pt); // добавим полученное к слою с текущим треком //console.log(savedLayers[currentTrackName].getLayers()[0]); } else savedLayers[currentTrackName].addData(resp.pt); // добавим полученное к слою с текущим треком } } } else { // лог не пишется if(typeof loggingIndicator != 'undefined'){ // лампочка и переключатель в интерфейсе if(loggingSwitch.checked){ // этот клиент сказал писать трек, состояние loggingSwitch восстанавливается из куки в index loggingIndicator.style.color='red'; loggingIndicator.innerText='\u2B24'; loggingRun(); // попытаемся запустить запись трека } else { loggingIndicator.style.color=''; loggingIndicator.innerText=''; if(currentWaitTrackUpdateProcess){ clearInterval(currentWaitTrackUpdateProcess); currentWaitTrackUpdateProcess = null; //console.log('[updateCurrTrack] Не должно быть currentWaitTrackUpdateProcess, но он был. Убили, запускаем.'); } if(currTrackSwitch.checked) startCurrentWaitTrackUpdateProcess(); // Текущий трек всегда показывается } } } } } // end function updateCurrTrack // // Функции рисования маршрутов function routeControlsDeSelect() { // сделаем невыбранными кнопки управления рисованием маршрута. Они должны быть и так не выбраны, но почему-то... for(let element of document.getElementsByName('routeControl')){ element.checked=false; element.disabled=true; }; }; // end function routeControlsDeSelect function pointsControlsDisable(){ for(let button of pointsButtons.querySelectorAll('button')){ // кнопки установки маркеров button.disabled = true; }; }; // end function pointsControlsDisable function pointsControlsEnable(){ for(let button of pointsButtons.querySelectorAll('button')){ // кнопки установки маркеров let gpxtype = button.id.substring(9); // id начинаются с "ButtonSet", а дальше, например, point: ButtonSetpoint //console.log('[pointsControlsEnable] button',gpxtype,button); button.onclick = function (event) {createEditableMarker(getGPXicon(gpxtype));}; button.disabled = false; }; }; // end function pointsControlsEnable function getGPXicon(gpxtype){ /* вообще-то, здесь должно быть обращение к iconServer из leaflet-omnivore, но пока так*/ let iconName = gpxtype+'Icon'; return window[iconName]; }; // end function getGPXicon function delShapes(realy,inLayer=null) { /* Удаляет полилинии в состоянии редактирования, если realy = true возвращает число таких объектов. Полилинии находятся в L.LayerGroup currentRoute. Мы не знаем, что такое currentRoute, и это может быть как dravingLines (L.LayerGroup с нарисованными локально объектами), так и ранее загруженный svg. При этом, как минимум в случае svg, эта L.LayerGroup сама состоит (только) из L.LayerGroup, в которых, в свою очередь, находится искомое. */ if(!inLayer) inLayer = currentRoute; //console.log('[delShapes] inLayer:',inLayer); let edEnShapesCntr=0; let needUpdateSuperclaster = false; for(let layer of inLayer.getLayers()){ if(layer instanceof L.LayerGroup) { // это layerGroup //if("getLayers" in layer) { // это layerGroup edEnShapesCntr += delShapes(realy,layer); } else { // это что-то ещё if(typeof layer.editEnabled === 'function' && layer.editEnabled()){ // оно редактируется сейчас edEnShapesCntr++; //console.log('[delShapes] editabled layer',layer); if(realy) { //if('getLatLngs' in layer) layer.editor.deleteShapeAt(layer.getLatLngs()[0]); // Мутный способ убрать слой с экрана, но я не вижу, как иначе. if(layer instanceof L.Path) { layer.editor.deleteShapeAt(layer.getLatLngs()[0]); // Мутный способ убрать слой с экрана, но я не вижу, как иначе. } else { needUpdateSuperclaster = removeFromSuperclaster(inLayer,layer); // могут быть кластеризованные точки, а так -- достаточно removeLayer } inLayer.removeLayer(layer); // удалим слой из LayerGroup //console.log('[delShapes] из inLayer ',inLayer._leaflet_id,inLayer,'удалён объект',layer._leaflet_id,layer); layer = null; // это приведёт к быстрому удалению объекта сборщиком мусора? Обычно оно не успевает... } } } } //console.log('[delShapes] needUpdateSuperclaster:',needUpdateSuperclaster); if(needUpdateSuperclaster) updClaster(inLayer); // обновим один раз за все удаления return edEnShapesCntr; }; // end function delShapes function tooggleEditRoute(e,flavor=null) { /* Переключает режим редактирования Обычно обработчик клика по линии flavor == true - включить редактирование flavor == false - выключить редактирование flavor == null - переключить редактирование */ //console.log('tooggleEditRoute start by anymore',e); // Щёлкнуть могли либо по нарисованному локально объекту (в том числе -- и по восстановленному из куки) // либо по загруженному gpx if(editorEnabled===false) { //console.log('[tooggleEditRoute] Редактирование запрещено'); return; } let target; if(e.target) target = e.target; // вызвали как обработчик события. В этом языке this почему-то currentTarget (текущий обработчик события в процессе всплытия), а не current (тот, кто инициировал событие). Поэтому лучше явно. else target = e; // вызвали просто как функцию // Это нужно делать до переноса target в другой слой, иначе у Leaflet.Editable съезжает крыша. switch(flavor){ case true: target.enableEdit(); break; case false: target.disableEdit(); break; default: target.toggleEdit(); // оно Leaflet.Editable. Собственно, это и есть переключение состояния. }; let layerName = ''; //console.log('[tooggleEditRoute] target',target); //console.log('[tooggleEditRoute] target.feature:',JSON.stringify(target.feature)); //console.log('[tooggleEditRoute] savedLayers:',savedLayers); if(dravingLines && dravingLines.hasLayerRecursive(target)){ // Щёлкнули по одному из нарисованных объектов. hasLayerRecursive потому что omnivore импортирует gpx как L.LayerGroup с