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,092 lines (1,015 loc) • 152 kB
JavaScript
"use strict"
/* Функции
onBodyLoad()
mapListPopulate()
listPopulate()
getCookie(name)
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)
createEditableMarker(Icon)
doSaveMeasuredPaths()
doRestoreMeasuredPaths()
bindPopUptoEditable(layer)
saveGPX() Сохраняет на сервере маршрут из объекта currentRoute
toGPX(geoJSON,createTrk) Create gpx route or track (createTrk==true) from geoJSON object
String.prototype.encodeHTML = function ()
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=' ')
MOBalarm()
setMOBpopup(layer)
createMOBpointMarker(mobMarkerJSON)
clearCurrentStatus()
MOBclose()
realMOBclose()
delMOBmarker()
mobMarkerDragendFunction(event)
mobMarkerClickFunction(event)
sendMOBtoServer()
MOBtoGeoJSON(MOBdata)
GeoJSONtoMOB(mobMarkerJSON,status,label)
distCirclesUpdate() Устанавливает диаметр и подписи кругов дистанции
distCirclesToggler() включает/выключает показ окружностей дистанции по переключателю в интерфейсе
windSwitchToggler()
windSymbolUpdate()
realWindSymbolUpdate(direction=0,speed=0)
restoreDisplayedRoutes()
chkDisplayedList(List,Displayed) Проверим соответствие списков
currentTrackUpdate() загружает трек, делает его показываемым и обновляет по мере записи
loadScriptSync(scriptURL) Синхронная загрузка javascript
bearing(latlng1, latlng2)
atou(b64) ASCII to Unicode (decode Base64 to original data)
utoa(data) Unicode to ASCII (encode data to Base64)
generateUUID()
arrayHasOnly
getSelfPathC Получает с сервера path, синхронно
realtime(dataUrl,fUpdate)
Классы
L.Control.CopyToClipboard
hasLayerRecursive(what)
eachLayerRecursive()
/////////////////////////// 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 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) {
/* Создаёт leaflet lauer с именем, содержащемся в mapParm, и заносит его на карту
Для SignalK mapname -- это identifier в смысле chart-plugin.
Делает запрос к SignalK для получения параметров карты
Если в имени карты есть EPSG3395 - делает слой в проекции с пересчётом с помощью L.tileLayer.Mercator
*/
mapname=mapname.trim(mapname);
// Всегда будем спрашивать параметры карты
let mapParm = new Array(); // переменная для параметров карты
const xhr = new XMLHttpRequest();
xhr.open('GET', '/signalk/v1/api/resources/charts/'+mapname, false); // Подготовим синхронный запрос
xhr.send();
if (xhr.status == 200) { // Успешно
try {
const skMapParm = JSON.parse(xhr.responseText); // параметры карты
mapParm.identifier=mapname;
mapParm.name=skMapParm.name;
mapParm.ext=skMapParm.format;
mapParm.minZoom=skMapParm.minzoom;
mapParm.maxZoom=skMapParm.maxzoom;
mapParm.tileCacheURI=skMapParm.tilemapUrl;
mapParm.data=skMapParm.data; // Этого там нет, и неизвестно, будет ли
mapParm.mapboxStyle=skMapParm.mapboxStyle; // имя файла стиля mapbox, для векторных тайлов. Этого там нет, и неизвестно, будет ли
}
catch(err) { //
console.error('Get chart '+mapname+' metainfo error:',err);
}
}
// javascript в загружаемом источнике на открытие карты
//console.log(mapParm);
if(mapParm['data'] && mapParm['data']['javascriptOpen']) eval(mapParm['data']['javascriptOpen']);
// Загружаемая карта - многослойная?
if(Array.isArray(additionalTileCachePath)) { // глобальная переменная - дополнительный кусок пути к талам между именем карты и /z/x/y.png Используется в версионном кеше, например, в погоде. Без / в конце, но с / в начале, либо пусто. Например, Weather.php. Присваивается в javascriptOpen в параметрах карты. Или ещё где-нибудь.
let currZoom;
if(savedLayers[mapname]) {
if(savedLayers[mapname].options.zoom) currZoom = savedLayers[mapname].options.zoom;
savedLayers[mapname].remove();
}
savedLayers[mapname]=L.layerGroup();
if(currZoom) savedLayers[mapname].options.zoom = currZoom;
for(let addPath of additionalTileCachePath) {
let tileCacheURIthis = mapParm.tileCacheURI+addPath; //
if(mapParm['ext']) tileCacheURIthis = tileCacheURIthis.replace('{ext}',mapParm['ext']); // при таком подходе можно сделать несколько слоёв с одним запросом параметров
//console.log(tileCacheURIthis);
//console.log('mapname=',mapname,savedLayers[mapname]);
if((mapParm['epsg']&&String(mapParm['epsg']).indexOf('3395')!=-1)||(mapParm.name.indexOf('EPSG3395')!=-1)||(mapParm.identifier.indexOf('EPSG3395')!=-1)) {
savedLayers[mapname].addLayer(L.tileLayer.Mercator(tileCacheURIthis, {minZoom:mapParm.minZoom,maxZoom:mapParm.maxZoom}));
}
else if(mapParm['mapboxStyle']) { // векторные тайлы
savedLayers[mapname].addLayer(L.mapboxGL({style: mapParm['mapboxStyle'],minZoom:mapParm.minZoom}));
}
else {
savedLayers[mapname].addLayer(L.tileLayer(tileCacheURIthis, {minZoom:mapParm.minZoom,maxZoom:mapParm.maxZoom}));
}
}
}
else {
let tileCacheURIthis = mapParm.tileCacheURI; //
if(mapParm['ext']) tileCacheURIthis = tileCacheURIthis.replace('{ext}',mapParm['ext']); // при таком подходе можно сделать несколько слоёв с одним запросом параметров
//console.log(tileCacheURIthis);
if((mapParm['epsg']&&String(mapParm['epsg']).indexOf('3395')!=-1)||(mapParm.name.indexOf('EPSG3395')!=-1)||(mapParm.identifier.indexOf('EPSG3395')!=-1)) {
if(!savedLayers[mapname]) savedLayers[mapname] = L.tileLayer.Mercator(tileCacheURIthis, {minZoom:mapParm.minZoom,maxZoom:mapParm.maxZoom});
}
else if(mapParm['mapboxStyle']) { // векторные тайлы
if(!savedLayers[mapname]) savedLayers[mapname] = L.mapboxGL({style: mapParm['mapboxStyle'],minZoom:mapParm.minZoom});
}
else {
if(!savedLayers[mapname]) savedLayers[mapname] = L.tileLayer(tileCacheURIthis, {minZoom:mapParm.minZoom,maxZoom:mapParm.maxZoom});
}
}
//console.log(savedLayers[mapname]);
// установим текущий масштаб в пределах возможного для загружаемой карты
if(! savedLayers[mapname].options.zoom) {
let currZoom = map.getZoom();
if(mapParm['maxZoom'] < currZoom) {
map.setZoom(mapParm['maxZoom']);
savedLayers[mapname].options.zoom = currZoom;
}
else if(mapParm['minZoom'] > currZoom) {
map.setZoom(mapParm['minZoom']);
savedLayers[mapname].options.zoom = currZoom;
}
else savedLayers[mapname].options.zoom = false;
}
// javascript в загружаемом источнике на закрытие карты
if(mapParm['data'] && mapParm['data']['javascriptClose']) savedLayers[mapname].options.javascriptClose = mapParm['data']['javascriptClose'];
// Наконец, покажем
savedLayers[mapname].addTo(map);
} // end function displayMap
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) {
/* рисует маршрут или места с именем routeName
global routeDirURI map window
*/
var routeName = routeNameNode.innerText.trim();
var options = {featureNameNode : routeNameNode};
if( savedLayers[routeName]) {
savedLayers[routeName].addTo(map); // нарисуем его на карте.
}
else {
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);
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.php
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) {
/* Переключает режим редактирования
Обычно обработчик клика по линии
*/
//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; // вызвали просто как функцию
let layerName = '';
currentRoute = null;
//console.log('[tooggleEditRoute] target',target);
//console.log('[tooggleEditRoute] savedLayers:',savedLayers);
if(dravingLines.hasLayerRecursive(target)){ // Щёлкнули по одному из нарисованных объектов. hasLayerRecursive потому что omnivore импортирует gpx как L.LayerGroup с двумя слоями: точки и всё остальное
//console.log('[tooggleEditRoute] Щёлкнули на объекте',target._leaflet_id,target,'в dravingLines',dravingLines._leaflet_id,dravingLines);
currentRoute = dravingLines;
layerName = new Date().toJSON(); // запишем в поле ввода имени дату
}
else {
for (layerName in savedLayers) { // нет способа определить, в какой layerGroup находится layer, но у нас все показываемые слои хранятся в массиве savedLayers
//console.log('[tooggleEditRoute] layerName=',layerName);
if((savedLayers[layerName] instanceof L.LayerGroup) && savedLayers[layerName].hasLayerRecursive(target)){
//console.log('[tooggleEditRoute] Щёлкнули на объекте',target._leaflet_id,target,'в',savedLayers[layerName]._leaflet_id,layerName,savedLayers[layerName]);
currentRoute = savedLayers[layerName];
routeSaveName.value = layerName; // запишем в поле ввода имени имя загруженного файла
break;
}
}
}
if(!currentRoute) {
console.log('[tooggleEditRoute] Не удалось определить currentRoute, облом.');
return;
}
//console.log('[tooggleEditRoute] target:',target,'currentRoute:',currentRoute,'dravingLines:',dravingLines)
target.toggleEdit(); // оно Leaflet.Editable
if(target.editEnabled()) { // если включено редактирование
//console.log('[tooggleEditRoute] Редактирование включили');
routeEraseButton.disabled=false; // - сделать доступной кнопку Удалить
if(!routeSaveName.value) routeSaveName.value = layerName; // имя файла для сохранения
// здесь устанавливается выключение режима редактирования по изменению и покиданию
// поля "описание объекта" в редакторе маршрутов
// Не знаю, хорошая ли это идея, но я со временем забыл, что для сохранения названия и описания объекта
/*/ нужно завершить редактирование этого объекта.
editableObjectDescr.onchange = function (){
tooggleEditRoute(target);
//console.log("Выключено редактирование объекта",target)
};*/
if((!routeSaveDescr.value) && currentRoute.properties && currentRoute.properties.desc) routeSaveDescr.value = currentRoute.properties.desc;
if(target.feature && target.feature.properties && target.feature.properties.name) editableObjectName.value = target.feature.properties.name;
if(target.feature && target.feature.properties && target.feature.properties.desc) editableObjectDescr.value = target.feature.properties.desc;
if(target instanceof L.Marker){
//console.log('[tooggleEditRoute] target is instanceof L.Marker',target);
routeCreateButton.disabled=true; // - сделать недоступной кнопку Начать
pointsControlsEnable(); // включим кнопки точек
target.setOpacity(0.4);
target.options.draggable = true; // сделаем маркер перемещаемым
const gpxtype = target.feature.properties.type;
//console.log('[tooggleEditRoute] gpxtype=',gpxtype,pointsButtons.querySelectorAll('button'));
for(let button of pointsButtons.querySelectorAll('button')){
if(button.id != 'ButtonSet'+gpxtype) {
button.disabled = true;
}
else {
button.onclick = function (event) {
tooggleEditRoute(target);
button.onclick = function (event) {createEditableMarker(target.getIcon());};
};
}
}
}
else {
pointsControlsDisable(); // отключить кнопки точек
routeContinueButton.disabled=false; // - сделать доступной кнопку Продолжить
}
}
else {
//console.log('[tooggleEditRoute] Редактирование выключили');
editableObjectDescr.onchange = null;
if(delShapes(false)) routeEraseButton.disabled=false; // если есть редактируемые слои в currentRoute
else { //
//console.log('[tooggleEditRoute] нет редактируемых слоёв: как бы завершаем редактирование currentRoute с именем',layerName,currentRoute);
if(!target.feature) target.feature = {};
if(!target.feature.properties) target.feature.properties = {};
target.feature.properties.name = editableObjectName.value;
target.feature.properties.desc = editableObjectDescr.value;
bindPopUptoEditable(target);
// Автоматическое сохранение ранее загруженного gpx по прекращению редактирования.
// в результате поведение редактирования файла с сервера такое же, как и редактирование локального.
// Раз уж они выглядят одинаково.
// А хорошая ли это идея?
if(currentRoute.properties && (routeSaveName.value == currentRoute.properties.fileName)){ // мы редактировали ранее загруженный файл
//console.log('[tooggleEditRoute] Сохраняется файл',currentRoute.properties.fileName);
//saveGPX();
}
else {
//console.log('[tooggleEditRoute] Сохраняется кука');
doSaveMeasuredPaths();
};
routeCreateButton.disabled=false; // - сделать доступной кнопку Начать
routeEraseButton.disabled=true; // - сделать недоступной кнопку Удалить
routeContinueButton.disabled=true; // - сделать недоступной кнопку Продолжить
if(editorEnabled==='maybe') editorEnabled=false; // панель закрыли во время редактирования, потом редактирование завершили
//currentRoute = null; // иначе saveGPX не сработает
//routeSaveName.value = ''; // если нет автоматического сохранения gpx, то надо оставить
//routeSaveDescr.value = '';
editableObjectName.value = '';
editableObjectDescr.value = '';
}
if(target instanceof L.Marker){
//console.log('[tooggleEditRoute] target is instanceof L.Marker');
target.setOpacity(0.7);
target.options.draggable = false; // сделаем маркер не перемещаемым
const gpxtype = target.feature.properties.type;
for(let button of pointsButtons.querySelectorAll('button')){ // кнопки установки маркеров
button.disabled = false;
if(button.id == 'ButtonSet'+gpxtype) { // кнопка, по которой был создан этот маркер
button.onclick = function (event) {createEditableMarker(target.getIcon());}; // вернём стандартное действие -- создание маркера
}
};
}
else pointsControlsEnable();
}
} // end function tooggleEditRoute
function createEditableMarker(Icon){
if(!currentRoute) currentRoute = dravingLines; //
let gpxtype = Icon.options.iconUrl.substring(Icon.options.iconUrl.lastIndexOf('/')+1,Icon.options.iconUrl.lastIndexOf('.png'));
let layer = map.editTools.startMarker(centerMarkMarker.getLatLng(),{
icon: Icon,
opacity: 0.5
}).addTo(currentRoute);
layer.feature = {type: 'Feature',
properties: { // типа, оно будет JSONLayer
type: gpxtype,
},
};
layer.on('click',tooggleEditRoute);
//layer.on('editable:drawing:end', function(event) {
// console.log('layer.on [editable:drawing:end] event.layer:',event.layer);
//});
//layer.on('editable:enable',function(event){
//});
//layer.on('editable:disable',function(event){
//})
// прикалывает маркер в указанных координатах. Если не прикалывать -- в мобильных браузерах
// значёк сдвигается вместе со шторкой инструментальной панели и прикалывается там.
// с другой стороны, в старых браузерах он в этот момент не двигается по тапу, т.е., фактически
// приколот, хотя действия не было.
layer.editor.tools.stopDrawing();
//console.log('createEditableMarker',layer);
for(let button of pointsButtons.querySelectorAll('button')){
//console.log('[createEditableMarker] button.id=',button.id,'ButtonSet+gpxtype=','ButtonSet'+gpxtype);
if(button.id != 'ButtonSet'+gpxtype) {
button.disabled = true;
}
else {
button.onclick = function (event) {
//console.log('[button on click] layer:',layer);
tooggleEditRoute(layer);
button.onclick = function (event) {createEditableMarker(Icon);};
};
}
}
routeControlsDeSelect(); // отключим все кнопки рисования линии
routeEraseButton.disabled=false; // включим кнопку Стереть
if(!routeSaveName.value) routeSaveName.value = new Date().toJSON(); // запишем в поле ввода имени дату, если там ничего не было
} // end function createEditableMarker
function doSaveMeasuredPaths() {
/* сохранение в cookie отображаемых на карте маршрутов
Сохраняются только маршруты, не находящиеся в состоянии редактирования.
Предполагается, что это для сохранения маршрутов/замеров расстояний на конкретном устройстве
*/
let expires = new Date();
let toSave = L.geoJSON();
function findEditDisabled(layer){
//console.log('[doSaveMeasuredPaths][findEditDisabled] layer:',layer,layer instanceof L.LayerGroup,'eachLayer' in layer);
if(layer instanceof L.LayerGroup){
layer.eachLayer(findEditDisabled);
}
else {
if(('editEnabled' in layer) && !layer.editEnabled()){ // режим редактирования этого слоя выключен или отсутствует
//console.log('[doSaveMeasuredPaths][findEditDisabled] layer:',layer,layer.toGeoJSON());
let gj = layer.toGeoJSON();
if(!gj.type){
//console.log('[doSaveMeasuredPaths][findEditDisabled] метод toGeoJSON() не добавляет в создаваемый GeoJSON свойство type = "Feature", если преобразуется объект типа L.Marker',gj);
gj.type = 'Feature';
}
toSave.addData(gj);
expires.setTime(expires.getTime() + (60*24*60*60*1000)); // протухнет через два месяца
}
}
} // end function findEditDisabled
//console.log('[doSaveMeasuredPaths] toSave original:',toSave);
dravingLines.eachLayer(findEditDisabled);
toSave = toSave.toGeoJSON(); // здесь я реально не понял. А оно не geoJSON? Оно не GeoJSON. Оно LayerGroup
toSave.properties = dravingLines.properties; // на самом деле -- чисто чтобы там было properties, оно нигде не используется
//console.log('[doSaveMeasuredPaths] toSave:',toSave);
toSave = toGPX(toSave); // сделаем gpx
//console.log('[doSaveMeasuredPaths] Save to cookie GaladrielMapMeasuredPaths',toSave,expires.getTime()-Date.now());
toSave = utoa(toSave); // кодируем в Base64, потому что xml нельза сохранить в куке
// если expires осталась сейчас -- кука удалится, иначе -- поставится.
//document.cookie = "GaladrielMapMeasuredPaths="+toSave+"; expires="+expires+"; path=/; SameSite=Lax;"; // если сечас и нет, чего сохранять - грохнем куки
storageHandler.save('RestoreMeasuredPaths',toSave);
//console.log('[doSaveMeasuredPaths] document.cookie:',document.cookie);
} // end function doSaveMeasuredPaths
function doRestoreMeasuredPaths() {
/*Global drivedPolyLineOptions*/
//let RestoreMeasuredPaths = getCookie('GaladrielMapMeasuredPaths');
let RestoreMeasuredPaths = storageHandler.restore('RestoreMeasuredPaths'); // storageHandler from galadrielmap.js
//console.log('[doRestoreMeasuredPaths] RestoreMeasuredPaths=',RestoreMeasuredPaths);
if(RestoreMeasuredPaths) {
try { // в принципе, там может быть фигня, но главное -- та же кука от старой версии приведёт к облому
RestoreMeasuredPaths = atou(RestoreMeasuredPaths); // восстановим из base64
}
catch {
return;
}
//console.log('[doRestoreMeasuredPaths] Restore from cookie',RestoreMeasuredPaths);
dravingLines.clearLayers();
dravingLines = omnivore.gpx.parse(RestoreMeasuredPaths); // leaflet-omnivore.js
//console.log('[doRestoreMeasuredPaths] dravingLines',dravingLines);
dravingLines.eachLayerRecursive(function (layer){
//console.log('[doRestoreMeasuredPaths] layer',layer);
if(layer.feature && (layer.feature.geometry.type == 'LineString' || layer.feature.geometry.type == 'Line')){
layer.options.color = '#FDFF00';
}
});
dravingLines.addTo(map);
}
} // end function doRestoreMeasuredPaths
function bindPopUptoEditable(layer){
// Подпись - Tooltip
let tooltip = layer.getTooltip();
if(tooltip){
if(layer.feature.properties.name) {
//console.log('[bindPopUptoEditable] изменение tooltip',tooltip);
layer.setTooltipContent(layer.feature.properties.name);
}
else layer.unbindTooltip();
}
else {
if(layer.feature.properties.name) {
layer.unbindTooltip();
layer.bindTooltip(layer.feature.properties.name,{
permanent: true, // всегда показывать
direction: 'auto',
//direction: 'left',
//offset: [-16,-25],
//offset: [-32,0],
className: 'wpTooltip', // css class
opacity: 0.75
});
}
}
// popUp
let popUpHTML = '';
if(layer.feature.properties.number) popUpHTML = " <span style='font-size:120%;'>"+layer.feature.properties.number+"</span> "+popUpHTML;
if(layer.feature.properties.name) popUpHTML = "<b>"+layer.feature.properties.name+"</b> "+popUpHTML;
if(layer instanceof L.Marker) {
let lat = Math.round(layer.getLatLng().lat*10000)/10000; // широта
let lng = Math.round(layer.getLatLng().lng*10000)/10000; // долгота
if(!popUpHTML) popUpHTML = lat+" "+lng;
popUpHTML = "<span style='font-size:120%'; onClick='doCopyToClipboard(\""+lat+" "+lng+"\");'>" +popUpHTML+ "</span><br>";
}
if(layer.feature.properties.cmt) popUpHTML += "<p>"+layer.feature.properties.cmt+"</p>";
if(layer.feature.properties.desc) popUpHTML += "<p>"+layer.feature.properties.desc.replace(/\n/g, '<br>')+"</p>"; // gpx description
if(layer.feature.properties.ele) popUpHTML += "<p>Alt: "+layer.feature.properties.ele+"</p>"; // gpx elevation
//popUpHTML += getLinksHTML(feature); // приклеим ссылки Пока не реализовано
layer.unbindPopup(); // если, допустим, описание было, а потом не стало
if(popUpHTML) {
//console.log('[bindPopUptoEditable] binding popup',popUpHTML);
layer.bindPopup(popUpHTML+'<br>');
}
} // end function bindPopUptoEditable
function saveGPX() {
/* Сохраняет на сервере маршрут из объекта currentRoute. currentRoute -- это или нарисованный
локально объект, или отредактированный gpx
*/
if(!currentRoute) { // глобальная переменная, присваивается в tooggleEditRoute, типа - по щелчку на маршруте
routeSaveMessage.innerHTML = 'Error - no route selected.'
return;
}
//console.log('[saveGPX] currentRoute:',currentRoute);
//console.log('[saveGPX] Сохраняется файл',currentRoute.properties.fileName);
function collectSuperclasterPoints(layerGroup){
//console.log('[collectSuperclasterPoints] layerGroup:',layerGroup);
let pointsFeatureCollection = []; //
for(const layer of layerGroup.getLayers()){
if('supercluster' in layer) { // это superclaster'изованный слой, с точками, надо полагать, ранее положенными в свойство layer.supercluster
//console.log('[collectSuperclasterPoints] layer.supercluster.points:',layer.supercluster.points);
pointsFeatureCollection = pointsFeatureCollection.concat(layer.supercluster.points);
}
if(layer instanceof L.LayerGroup) { // это LayerGroup
pointsFeatureCollection = pointsFeatureCollection.concat(collectSuperclasterPoints(layer));
}
}
//console.log('[collectSuperclasterPoints] pointsFeatureCollection:',pointsFeatureCollection);
return pointsFeatureCollection;
} // end function collectSuperclasterPoints
let fileName = routeSaveName.value; // имя файла для сохранения, поле в интерфейсе
if(! fileName) { // внезапно имени нет, хотя в index поле заполняется
fileName = new Date().toJSON();
routeSaveName.value = fileName;
}
if(!(currentRoute instanceof L.LayerGroup)) currentRoute = new L.LayerGroup([currentRoute]); // попробуем сменть тип на layerGroup, но это обычно боком выходит, потому что всё же layergroup не layer. Да, впрочем, нормально?
// Теперь делаем JSON, из которого сделаем gpx
// Сначала соберём в pointsFeatureCollection реальные точки из данных superclaster
// поскольку мы хотим toGeoJSON() все имеющиеся точки, а слой может быть superclaster, то будем доставать точки из supercluster'а
let pointsFeatureCollection = collectSuperclasterPoints(currentRoute); //
//console.log('[saveGPX] pointsFeatureCollection:',pointsFeatureCollection);
let route = currentRoute.toGeoJSON(); // сделаем объект geoJSON. Очевидно, это новый объект?
if(!('properties' in route)) route.properties = {};
//route.properties.fileName = fileName; // имя файла. А нафига?
if(routeSaveDescr.value.trim()) route.properties.desc = routeSaveDescr.value; // общий комментарий
route.properties.time = new Date().toISOString();
route.properties.xmlns = "http://www.topografix.com/GPX/1/1";
route.properties['xmlns:gpxx'] = "http://www8.garmin.com/xmlschemas/GpxExtensions/v3";
route.properties['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance";
route.properties['xsi:schemaLocation'] = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd https://www8.garmin.com/xmlschemas/GpxExtensions/v3 https://www8.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd";
for(let key in currentRoute.properties) { //
if(typeof route.properties[key] === 'undefined') route.properties[key] = currentRoute.properties[key];
}
//console.log('[saveGPX] currentRoute:',currentRoute);
//console.log('[saveGPX] route as geoJSON:',route);
// теперь выкинем точки, которые есть в supercluster, а потом добавим все точки из supercluster
// потому что при текущем масштабе некоторые точки из supercluster могли отображаться как точки,
// а не как значки supercluster
if(pointsFeatureCollection.length) { // это был supercluster, поэтому в geoJSON неизвестно, сколько оригинальных точек, а не все. Но у нас с собой было...
// выкинем все точки, присутствующие в pointsFeatureCollection
let pointsFeatureCollectionStrings = pointsFeatureCollection.map(function (point){
// а вот тут убъём все сохранённые маркеры
// из-за того, что JSON.stringify нельзя
// заставить что-то сделать с циклической структурой
point.properties.marker = undefined;
return JSON.stringify(point);
});
route.features = route.features.filter(function(feature){
// не сами кластеры, не точки, и точки, не входящие в pointsFeatureCollection
return (!feature.properties.cluster) && ((feature.geometry.type !== 'Point') || (! pointsFeatureCollectionStrings.includes(JSON.stringify(feature))));
});
//console.log('[saveGPX] JSON.stringify(route.features)',JSON.stringify(route.features));
// нифига не понятно, почему layer.supercluster.points -- это geoJSON? Видимо, потому, что
// в supercluster исходно загружаются не объекты leaflet, а GeoJSON Feature objects.
route.features = route.features.concat(pointsFeatureCollection); // теперь положим туда точки, ранее взятые в superclaster'е
}
//console.log('[saveGPX] route as geoJSON after:',route);
route = toGPX(route); //