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
JavaScript
"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 с