UNPKG

galadrielmap_sk

Version:

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

1,019 lines (970 loc) 120 kB
<!DOCTYPE html > <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" > <!-- tell the mobile browser to disable unwanted scaling of the page and set it to its actual size --> <script src="internationalisation/internationalisation.js" ></script> <!-- там определяются переменные, используемые в загружаемых скриптах --> <!-- Leaflet --> <link rel="stylesheet" href="leaflet/leaflet.css" type="text/css"> <script src="leaflet/leaflet.js"></script> <script src="Leaflet.RotatedMarker/leaflet.rotatedMarker.js"></script> <!-- Leaflet sidebar --> <link rel="stylesheet" href="leaflet-sidebar-v2/css/leaflet-sidebar.min.css" /> <script src="leaflet-sidebar-v2/js/leaflet-sidebar.min.js"></script> <script src="polycolor/polycolorRenderer.js"></script> <script src="value2color/value2color.js"></script> <link rel="stylesheet" href="leaflet-omnivorePATCHED/leaflet-omnivore.css" /> <script src="leaflet-omnivorePATCHED/leaflet-omnivore.js"></script> <script src="Leaflet.Editable/src/Leaflet.Editable.js"></script> <link rel="stylesheet" href="leaflet-measure-path/leaflet-measure-path.css" /> <script src="leaflet-measure-path/leaflet-measure-path.js"></script> <script src="L.TileLayer.Mercator/src/L.TileLayer.Mercator.js"></script> <script src="supercluster/dist/supercluster.js"></script> <link rel="stylesheet" href="leaflet-tracksymbolPATCHED/leaflet-tracksymbol.css"/> <script src="leaflet-tracksymbolPATCHED/leaflet-tracksymbol.js"></script> <script src="coordinate-parserPATCHED/coordinates.js"> </script> <script src="coordinate-parserPATCHED/validator.js"></script> <script src="coordinate-parserPATCHED/coordinate-number.js"></script> <script src="long-press-event/dist/long-press-event.min.js"></script> <script src="Leaflet.TextPath/leaflet.textpath.js"></script> <link rel="stylesheet" href="galadrielmap.css" type="text/css"> <!-- замена стилей --> <script src="galadrielmap.js"></script> <script src="options.js"></script> <title>GaladrielMap SignalK ed.</title> <!-- карта на весь экран --> <style> body { padding: 0; margin: 0; } html, body, #mapid { height: 100%; width: 100vw; } </style> </head> <body> <div id="sidebar" class="leaflet-sidebar collapsed"> <!-- Nav tabs --> <div class="leaflet-sidebar-tabs"> <ul role="tablist" id="featuresList"> <li id="homeTab"><a href="#home" role="tab"><img src="img/maps.svg" alt="menu" width="70%"></a></li> <li id="dashboardTab"><a href="#dashboard" role="tab"><img src="img/speed1.svg" alt="dashboard" width="70%"></a></li> <li id="tracksTab"><a href="#tracks" role="tab"><img src="img/track.svg" alt="tracks" width="70%"></a></li> <li id="measureTab" ><a href="#measure" role="tab"><img src="img/route.svg" alt="Create route" width="70%"></a></li> <li id="routesTab"><a href="#routes" role="tab"><img src="img/poi.svg" alt="Routes and POI" width="70%"></a></li> </ul> <ul role="tablist" id="settingsList"> <li id="MOBtab" style="margin-bottom:1.5em;"><a href="#MOB" role="tab"><img src="img/mob.svg" alt="activate MOB" width="70%"></a></li> <li><a href="#settings" role="tab"><img src="img/settings1.svg" alt="settings" width="70%"></a></li> </ul> </div> <!-- Tab panes --> <div class="leaflet-sidebar-content" id='tabPanes'> <!-- <div id='infoBox' style='font-size: 90%; position: absolute;'> </div> <script> infoBox.innerText='width: '+window.outerWidth+' height: '+window.outerHeight; </script> --> <!-- Карты --> <div class="leaflet-sidebar-pane" id="home" style="height:100%;"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"> <span id="homeHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <div style="min-height:92%;"> <br> <ul id="mapDisplayed" class='commonList'> </ul> <ul id="mapList" class='commonList'> <li hiden class="template" onClick="{selectMap(event.currentTarget)}"></li> </ul> </div> <button id="showMapsToggler" onClick='showMapsToggle();' style="width:90%;height:1.5rem;margin-bottom:1rem;"></button> </div> <!-- Приборы --> <div class="leaflet-sidebar-pane" id="dashboard" style="height:100%;"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"> <span id="dashboardHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <div class="big_symbol"> <!-- передвинуть карту на место курсора --> <div> <div style="line-height:0.6;" onClick="map.setView(cursor.getLatLng());"> <div style="font-size:50%;"><span id="dashboardSpeedTXT"></span></div><br> <div id='velocityDial'></div><br> <div style="font-size:50%;"><span id="dashboardSpeedMesTXT"></span></div> </div> <div id='depthDial' style="line-height:0.4;" onClick="map.setView(cursor.getLatLng());"> </div> <div style="line-height:0.6;" onClick="map.setView(cursor.getLatLng());"> <br><span style="font-size:50%;"><span id="dashboardCourseTXT"></span></span> <span style="font-size:30%; "><br><span id="dashboardCourseAltTXT"></span></span> </div> <div style=""> <span id='courseDisplay'></span> </div> <div style="font-size:50%;line-height:0.6;" onClick="doCopyToClipboard(lat+' '+lng);" > <br><span style="font-size:50%;" id="mobPosTXT"></span><br> <span style="font-size:30%;" id="mobPosAltTXT"></span> </div> <div style="font-size:50%;" onClick="doCopyToClipboard(lat+' '+lng);"> <span id='locationDisplay'></span> </div> </div> </div> <div id="positionTimeDisplay" style="float:left;position:relative;bottom:90%;left:-1rem;"></div> <div class="scaledText" style="text-align:center; position: absolute; bottom: 0;"> <span id="dashboardSpeedZoomTXT"></span> <span id='velocityVectorLengthInMnDisplay'></span> <span id="dashboardSpeedZoomMesTXT"></span>. </div> </div> <!-- Треки --> <div class="leaflet-sidebar-pane" id="tracks"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"> <span id="tracksHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <div style="margin: 1rem;"> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="loggingSwitch" onChange="loggingRun();" > <label class="onoffswitch-label" for="loggingSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <div style="padding:1rem 0 0 0;font-size:120%"> <span id="loggingIndicator" style="font-size:100%;"></span> <span id="loggingTXT"></span> </div> </div> <ul id="trackDisplayed" class='commonList'> </ul> <ul id="trackList" class='commonList'> <li hidden class="template" onClick='{selectTrack(event.currentTarget,trackList,trackDisplayed,displayTrack)}' id='trackLiTemplate' class='currentTrackName' title=''></li> </ul> </div> <!-- Расстояния --> <div class="leaflet-sidebar-pane" id="measure"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"> <span id="measureHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <!-- Кнопки создания/редактирования маршрута --> <div id='routeControls' class="routeControls" style="width:95%; padding:1rem 0 2rem; text-align: center;"> <input type="radio" name="routeControl" class='L' id="routeCreateButton" onChange=" pointsControlsDisable(); // отключить кнопки точек if(!currentRoute) currentRoute = dravingLines; // //console.log('[Кнопка Начать] currentRoute:',currentRoute._leaflet_id,'dravingLines:',dravingLines._leaflet_id); let layer = map.editTools.startPolyline(false,drivedPolyLineOptions.options); layer.options.color = '#FDFF00'; layer.feature = drivedPolyLineOptions.feature; layer.on('editable:editing', function (event){event.target.updateMeasurements();}); // обновлять расстояния при редактировании //layer.on('click', L.DomEvent.stop).on('click', tooggleEditRoute); layer.on('click',tooggleEditRoute); layer.addTo(currentRoute); routeEraseButton.disabled=false; //if(!routeSaveName.value || Date.parse(routeSaveName.value)) routeSaveName.value = new Date().toJSON(); // запишем в поле ввода имени дату, если там ничего не было или была дата if(!routeSaveName.value) routeSaveName.value = new Date().toJSON(); // запишем в поле ввода имени дату, если там ничего не было //console.log('[Кнопка Начать] layer:',layer); " > <label for="routeCreateButton"><span id="routeControlsBeginTXT"></span></label> <input type="radio" name="routeControl" class='R' id="routeContinueButton" onChange=" // по нажатию кнопки создаётся однократно срабатываемый обработчик клика // на вершине объекта editable map.once('editable:vertex:click', function f(e) { // это CancelableVertexEvent //console.log(e); //console.log(e.vertex); e.cancel(); // прекратить дальнейшую обработку //e.vertex.split(); e.vertex.continue(); routeCreateButton.checked=true; }); " > <label for="routeContinueButton"><span id="routeControlsContinueTXT"></span></label><br> <div id='pointsButtons'> <br> <button id='ButtonSetpoint' onClick='createEditableMarker(pointIcon);' class='pointButton'><img src="leaflet-omnivorePATCHED/symbols/point.png" alt="ok" width="100%"></button> <button id='ButtonSetanchor' onClick='createEditableMarker(anchorIcon);' class='pointButton'><img src="leaflet-omnivorePATCHED/symbols/anchor.png" alt="ok" width="100%"></button> <button id='ButtonSetcaution' onClick='createEditableMarker(cautionIcon);' class='pointButton'><img src="leaflet-omnivorePATCHED/symbols/caution.png" alt="ok" width="100%"></button><br> <br> </div> <input id = 'editableObjectName' type="text" title="" placeholder='' size='255' style='width:90%;font-size:150%;'><br> <textarea id = 'editableObjectDescr' title="" rows='3' cols='255' placeholder='' style='width:87%;padding: 0.5rem 3%;'></textarea><br> <br> <input type="radio" name="routeControl" id="routeEraseButton" onChange=" delShapes(true); // удалим все редактируемые объекты routeControlsDeSelect(); // сделаем невыбранными кнопки управления рисованием маршрута routeCreateButton.disabled=false; // - сделать доступной кнопку Начать pointsControlsEnable(); // включим кнопки точек this.disabled=true; routeContinueButton.disabled=true; // раз не осталось редактируемых объектов, редактирование завершено? Сохраним. if(currentRoute==dravingLines) doSaveMeasuredPaths(); //else saveGPX(); // ?но загруженный файл не будем сохранять, потому что он тогда перезагрузится, и перестанет быть текущим редактируемым //currentRoute = null; // ?не будем считать, что редактирование завершено " > <label for="routeEraseButton"><span id="routeControlsClearTXT"></span></label> </div> <!-- Поиск места --> <div style="width:95%;"> <div style="margin:0;padding:0;"> <button onClick='goToPositionField.value += "°";goToPositionField.focus();' style="width:2rem;height:1.5rem;margin:0 0.7rem 0 0;"><span style="font-weight: bold; font-size:150%;">°</span></button> <button onClick='goToPositionField.value += "′";goToPositionField.focus();' style="width:2rem;height:1.5rem;margin:0 0.7rem 0 0;"><span style="font-weight: bold; font-size:150%;">′</span></button> <button onClick='goToPositionField.value += "″";goToPositionField.focus();' style="width:2rem;height:1.5rem;margin:0 0rem 0 0;"><span style="font-weight: bold; font-size:150%;">″</span></button><br> </div> <span id="routePosTXT"></span><br> <input id='goToPositionField' type="text" title="" size='12' style='width:70%;font-size:150%;'> <button id='goToPositionButton' class='okButton' onClick='flyByString(goToPositionField.value);' style="float:right;"><img src="img/ok.svg" alt="Ok" width="16px"></button><br> </div> <div style='width:98%;height:12rem;overflow:auto;margin:0.3rem 0;'> <ul id='geocodedList' class='commonList'> </ul> </div> <!-- Сохранение маршрута --> <div style="width:95%; padding: 1rem 0; text-align: center;"> <h3 id="routeSaveTitle"></h3> <input id = 'routeSaveName' type="text" title="" placeholder='' size='255' style='width:95%;font-size:150%;'> <textarea id = 'routeSaveDescr' title="" rows='5' cols='255' placeholder='' style='width:93%;padding: 0.5rem 3%;'></textarea> <button onClick=" saveGPX(); //currentRoute = null; //routeSaveName.value = ''; //routeSaveDescr.value = '';" " type='submit' class='okButton' style="float:right;"><img src="img/ok.svg" alt="Ok" width="16px"></button> <button onClick='routeSaveName.value=""; routeSaveDescr.value="";' type='reset' class='okButton' style="float:left;"><img src="img/no.svg" alt="clear" width="16px"></button> <div id='routeSaveMessage' style='margin: 1rem;'></div> </div> </div> <!-- Места и маршруты --> <div class="leaflet-sidebar-pane" id="routes"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"> <span id="routesHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <ul id="routeDisplayed" class='commonList'> </ul> <ul id="routeList" class='commonList'> <li hidden class="template" onClick='{selectTrack(event.currentTarget,routeList,routeDisplayed,displayRoute)}'></li> </ul> </div> <!-- MOB --> <div class="leaflet-sidebar-pane" style="height:90%;" id="MOB"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close" style="background-color:red;"><span id="mobTXT"></span><span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <div style="margin: 1rem 1rem;width:90%;text-align: center;"> <button onClick='MOBalarm();' style="width:75%;"><span style=""><span id="addMarkerTXT"></span></span></button> </div> <div class="big_symbol" style="line-height: normal;align-items: center;height:70%;" onClick="map.setView(currentMOBmarker.getLatLng());"> <!-- передвинуть карту на место текущего маркера MOB --> <div style=''><!-- объемлющий div необходим --> <div style="font-size:50%;"> <span style="font-size:50%;display:block;" id="bearingTXT"></span> <span style="font-size:40%;display:block;" id="altBearingTXT"></span> <span style="margin:0.5rem;display:block;" id='azimuthMOBdisplay'> </span> </div> <div style="font-size:75%;margin:1rem 0;"> <span style="font-size:40%;display:block;"><span id="distanceTXT"></span>, <span id="dashboardMeterMesTXT"></span></span> <span style="font-size:30%;display:block;" id="altDistanceTXT"></span> <span style="margin:0.5rem;display:block;" id='distanceMOBdisplay'> </span> <span style="font-size:75%;margin:0.5rem;display:block;" id='directionMOBdisplay'></span> </div> <div style="font-size:50%;" onClick="doCopyToClipboard(Math.round(currentMOBmarker.getLatLng().lat*10000)/10000+' '+Math.round(currentMOBmarker.getLatLng().lng*10000)/10000);" > <span style="font-size:50%;display:block;" id="dashboardPosTXT"></span> <span style="font-size:40%;display:block;" id="dashboardPosAltTXT"></span> <span style="margin:0.3rem;display:block;" id='locationMOBdisplay'></span> </div> </div> </div> <div style="position: absolute; bottom: 1rem;width:90%;text-align: center;"> <!-- Отбой --> <button onClick='delMOBmarker();' id='delMOBmarkerButton' style="width:80%;margin:1rem 0;font-size:75%;" disabled ><span style="" id="removeMarkerTXT"></span></button> <div> <a style="position:relative;left:-1rem;font-size:100%;color:gray;" onClick=' this.nextElementSibling.disabled=false; this.style.color="green"; '>&#x2B24;</a> <button onClick='realMOBclose();' style="width:75%;" disabled><span style="" id="cancelMOBTXT"></span></button> </div> </div> </div> <!-- Настройки --> <div class="leaflet-sidebar-pane" id="settings"> <h1 class="leaflet-sidebar-header leaflet-sidebar-close"><span id="settingsHeaderTXT"></span> <span class="leaflet-sidebar-close-icn"><img src="img/Triangle-left.svg" alt="close" width="16px"></span></h1> <div style="margin: 0.7em 1em;"> <!-- Следование за курсором --> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="followSwitch" onChange="noFollowToCursor=!noFollowToCursor; CurrnoFollowToCursor=noFollowToCursor;');" checked> <label class="onoffswitch-label" for="followSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%" id="settingsCursorTXT"></span> </div> <br> <div style="margin: 0.7em 1em;"> <!-- Текущий трек всегда показывается --> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="currTrackSwitch" onChange="loggingWait();" checked> <label class="onoffswitch-label" for="currTrackSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%" id="settingsTrackTXT"></span> </div> <br> <div style="margin: 0.7em 1em;"> <!-- Выбранные маршруты всегда показываются --> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="SelectedRoutesSwitch" onChange="" checked> <label class="onoffswitch-label" for="SelectedRoutesSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%" id="settingsRoutesAlwaysTXT"></span> </div> <br> <div style="margin: 0.7em 1em;"> <!-- Показывать окружности дистанции --> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="distCirclesSwitch" onChange="distCirclesToggler();" checked> <label class="onoffswitch-label" for="distCirclesSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%" id="settingsdistCirclesTXT"></span> </div> <br> <div style="margin: 0.7em 1em;"> <!-- Показывать символ ветра --> <div class="onoffswitch" style="float:right;margin: 1rem auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="windSwitch" onChange="windSwitchToggler();"> <label class="onoffswitch-label" for="windSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%" id="settingsdistWindTXT"></span> </div> <br> <div style="margin: 3em 1em 0.1em;"> <!-- Показ целей AIS --> <div class="onoffswitch" style="float:right;margin: 0 auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="DisplayAISswitch" onChange="watchAISswitching();"> <label class="onoffswitch-label" for="DisplayAISswitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%;" id="DisplayAIS_TXT"></span> </div> <br> <div style="margin: 3em 1em 0.1em;"> <!-- Сокрытие элементов управления --> <div class="onoffswitch" style="float:right;margin: 0 auto;"> <!-- Переключатель https://proto.io/freebies/onoff/ --> <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="hideControlsSwitch" value="onoffswitch" onChange="hideControlsToggler(this);"> <label class="onoffswitch-label" for="hideControlsSwitch"> <span class="onoffswitch-inner"></span> <span class="onoffswitch-switch"></span> </label> </div> <span style="font-size:120%;" id="hideControlsSwitchTXT"></span><br><br> <div> <div style="float: right;"> <input style="width:1em;" type="radio" name="hideControlPosition" value="topleft" disabled onChange="hideControlsToggler(this);"> <input style="width:1em;" type="radio" name="hideControlPosition" value="topmiddle" disabled onChange="hideControlsToggler(this);"> <input style="width:1em;" type="radio" name="hideControlPosition" value="topright" disabled onChange="hideControlsToggler(this);"><br> <input style="width:1em;" type="radio" name="hideControlPosition" value="leftmiddle" disabled onChange="hideControlsToggler(this);"> <input style="width:1em;float:right;" type="radio" name="hideControlPosition" value="rightmiddle" disabled onChange="hideControlsToggler(this);"> <span>&nbsp;</span><br> <input style="width:1em;" type="radio" name="hideControlPosition" value="bottomleft" disabled onChange="hideControlsToggler(this);"> <input style="width:1em;" type="radio" name="hideControlPosition" value="bottommiddle" checked disabled onChange="hideControlsToggler(this);"> <input style="width:1em;" type="radio" name="hideControlPosition" value="bottomright" disabled onChange="hideControlsToggler(this);"> </div> <span style="font-size:100%; margin:100% 0 100% 0;" id="hideControlsPositionTXT"></span> </div> </div> <br> <div style="margin: 3em 1em 0.1em;"> <!-- максимальная скорость обновления --> <div style="float:right;margin: 1rem auto;"> <input id='minWATCHintervalInput' type="text" pattern="[0-9]*" title="" size='4' style='width:3rem;font-size:175%;' onChange="minWATCHinterval=parseFloat(this.value); if(isNaN(minWATCHinterval)) minWATCHinterval=0; //console.log('Изменение, minWATCHinterval',minWATCHinterval); spatialWebSocketStop('Close socket to change WATCH interval'); watchAISstop('Close socket to change WATCH interval'); " > </div> <span style="font-size:120%;" id="minWATCHintervalTXT"></span> </div> </div> </div> </div><!-- end sidebar --> <div id="hideControl"> </div> <div id="mapid" ></div> </body> <script> "use strict"; // Глобальные переменные // Карта var showMapsList = storageHandler.restore('showMapsList') || []; // массив названий избранных карт var savedLayers = []; // массив для хранения объектов, когда они не на карте var additionalTileCachePath = ''; // дополнительный кусок пути к тайлам между именем карты и /z/x/y.png Используется в версионном кеше, например, в погоде. Без / в конце, но с / в начале, либо пусто. Присваивается в javascriptOpen в параметрах карты. Или ещё где-нибудь. var startCenter = storageHandler.restore('startCenter'); // storageHandler from galadrielmap.js if(! startCenter) startCenter = L.latLng(defaultCenter); // начальная точка из options.js var startZoom = storageHandler.restore('startZoom'); // storageHandler from galadrielmap.js if(! startZoom) startZoom = 12; // начальный масштаб var userMoveMap = true; // флаг для отделения собственных движений карты от пользовательских. Считаем все пользовательскими, и только где надо - выставляем иначе // ГПС var minWATCHinterval = storageHandler.restore('minWATCHinterval'); // Минимальный интервал, сек., с которым будут приходить данные от gpsdPROXY. Если 0 -- то по мере их получения от датчиков if(!minWATCHinterval) minWATCHinterval = 0; minWATCHintervalInput.value = minWATCHinterval; if(PosFreshBefore < (2*minWATCHinterval*1000+1000)) PosFreshBefore = 2*minWATCHinterval*1000+1000; // PosFreshBefore в options.js if(DepthFreshBefore < (2*minWATCHinterval*1000)) DepthFreshBefore = 2*minWATCHinterval*1000; // DepthFreshBefore в options.js if(WindFreshBefore < (2*minWATCHinterval*1000)) WindFreshBefore = 2*minWATCHinterval*1000; // WindFreshBefore в options.js var followToCursor = true; // карта следует за курсором Обеспечивает только паузу следования при перемещениях и масштабировании карты руками var noFollowToCursor = false; // карта никогда не следует за курсором Глобальное отключение следования. Само не восстанавливается. var CurrnoFollowToCursor = 1; // глобальная переменная для сохранения состояния var followPause = 10 * 1000; // пауза следования карты за курсором, когда карту подвинули руками, микросекунд var savePositionEvery = 10 * 1000; // будем сохранять положение каждые микросекунд локально в куку var followPaused; // объект таймера, который восстанавливает следование курсору if(!velocityVectorLengthInMn) velocityVectorLengthInMn = 10; // длинной в сколько минут пути рисуется линия скорости // Окружности дистанции distCirclesSwitch.checked = Boolean(storageHandler.restore('distCirclesSwitch')); // показывать окружности дистанции // AIS var vehicles = []; // list of visible by AIS data vehicle objects массив layers с целями // Пути и маршруты var editorEnabled = false; // семафор, что можно использовать редактирования // Путь var currentTrackServerURI = 'getlasttrkpt'; // адрес для подключения к сервису, отдающему сегменты текущего трека var trackDirURI = 'track'; // адрес каталога с треками var routeDirURI = 'route'; // адрес каталога с маршрутами var currentTrackName = ''; // имя текущего (пишущегося сейчас) трека var updateRouteServerURI = 'checkRoutes'; // url службы динамического обновления маршрутов currTrackSwitch.checked = Boolean(storageHandler.restore('currTrackSwitch')); SelectedRoutesSwitch.checked = Boolean(storageHandler.restore('SelectedRoutesSwitch')); // показывать выбранные маршруты loggingSwitch.checked = Boolean(storageHandler.restore('loggingSwitch')); // storageHandler from galadrielmap.js if(loggingSwitch.checked) loggingRun(); // запустим запись трека, если было указано запустить var currentRoute; // объект Editable, по которому щёлкнули. Типа, текущий. var globalCurrentColor = 0xFFFFFF; // цвет линий и значков кластеров после первого набора var currentTrackShowedFlag = false; // флаг, не показывается ли текущий путь. Если об этом спрашивать у Leaflet, то пока загружается трек, можно запустить его загрузку ещё раз пять. // Маршрут var drivedPolyLineOptions; var currentRoute; // L.layerGroup, по объекту Editable которого щёлкнули. Типа, текущий. {let weight; if(L.Browser.mobile && L.Browser.touch) weight = 13; // мобильный браузер else weight = 9; // стационарный браузер drivedPolyLineOptions = { options: { showMeasurements: true, // включить показ расстояний //color: '#FDFF00', weight: weight, opacity: 0.7, }, feature: {type: 'Feature', properties: { // типа, оно будет JSONLayer isRoute: true // укажем, что это путь }, }, }; } var dravingLines = L.layerGroup(); // слои, в которых, собственно, рисуются маршруты и путевые точки dravingLines.properties = {}; var goToPositionManualFlag = false; // флаг, что поле goToPositionField стали редактировать руками, и его не надо обновлять var distCircles = []; // круги дистанции, массив L.circle. Обращение к этому массиву может происходить сразу после инициализации карты. if(storageHandler.restore('WindSwitch') === null) windSwitch.checked = true; // показывать символ ветра else windSwitch.checked = Boolean(storageHandler.restore('WindSwitch')); // getCookie from galadrielmap.js // Dashboard var lat; // широта var lng; // долгота, округлённые до 4-х знаков var controlsList = []; // список control для сокрытия их с экрана // восстановление переключателя сокрытия всего hideControlsSwitch.checked = Boolean(storageHandler.restore('hideControlsSwitch')); if(storageHandler.restore('hideControlPosition')){ for(let radio of settings.querySelectorAll('input[type="radio"][name="hideControlPosition"]')){ if(radio.value == storageHandler.restore('hideControlPosition')) { radio.checked = true; } else radio.checked = false; }; }; // MOB var currentMOBmarker; // main output data var upData = {}; //var vesselSelf = getCookie('GaladrielvesselSelf'); var vesselSelf = storageHandler.restore('vesselSelf'); // storageHandler from galadrielmap.js //var instanceSelf = getCookie('GaladrielMapInstance'); // идентификатор экземпляра программы var instanceSelf = storageHandler.restore('instanceSelf'); // storageHandler from galadrielmap.js if(!instanceSelf) { instanceSelf = '-' + generateUUID(); // - в начале - чтобы начало не совпало с mmsi let expires = new Date(); expires.setTime(expires.getTime() + (24*60*60*1000)); // протухнет через сутки //document.cookie = "GaladrielMapInstance="+instanceSelf+"; expires="+expires+"; path=/; SameSite=Lax;"; storageHandler.save('instanceSelf',instanceSelf); } // Кластеризация точек var superclusterRadius = 40; // px var lastSuperClusterUpdatePosition = [[0,0],0]; // [<LatLng>,<zoom>] точка и масштаб последнего пересчёта supercluster //var collisionVessels = {}; ///////// for collision test purpose ///////// // Поехали // подготовка интерфейса, списков карт и треков, etc. onBodyLoad() // Определим карту var map = L.map('mapid', { center: startCenter, zoom: startZoom, attributionControl: false, zoomControl: false, editable: true } ); // Controls // Zoom в правом верхнем углу controlsList.push(L.control.zoom({ position:'topright' }).addTo(map)); // Версия и пр. в правом нижнем углу controlsList.push(L.control.attribution({ prefix: '<a href="https://youtu.be/kwMt4rjgsJs" target=”_blank”><i>имевший цель, но чуждый смысла</i></a>' }).addTo(map)); // Шкала масштаба //controlsList.push( L.control.scale({ position: 'bottomleft', maxWidth: 200, imperial: false }).addTo(map) //); // control для записывания в clipboard var copyToClipboard = new L.Control.CopyToClipboard({ // класс определён в galadrielmap.js position: 'bottomright' }); // на карту не добавляется // Добавим копирование содержимого goToPositionField. На него навешивается много обработчиков goToPositionField.addEventListener('long-press', function(e){doCopyToClipboard(goToPositionField.value);}); // при получении фокуса - прекратить обновление // Панель управления var sidebar = L.control.sidebar('sidebar',{ container: 'sidebar', }).addTo(map); controlsList.push(sidebar); sidebar.on("content", function(event){ // Событие открытия? панели switch(event.id){ // какую вкладку открыли case 'tracks': // треки loggingCheck(); break; case 'measure': // рисование маршрута centerMarkOn(); // включить крестик в середине if(CurrnoFollowToCursor === 1)CurrnoFollowToCursor = noFollowToCursor; // запомним состояние глобального признака следования за курсором, если ещё не запоминали noFollowToCursor = true; // отключим следование за курсором editorEnabled = true; // разрешим редактирования routeCreateButton.disabled=false; // - сделать доступной кнопку Начать pointsControlsEnable(); // включим кнопки точек break; case 'routes': // треки // обновим список маршрутов, асинхронно listPopulate(routeList,routeDirURI,false,true,function(){ const routeListLi = routeList.querySelectorAll('li'); routeDisplayed.querySelectorAll('li').forEach(function (displayedLi){ //console.log('displayedLi:',displayedLi.id); for(const li of routeListLi){ //console.log('\trouteList li',li.id); if(displayedLi.id==li.id){ li.remove(); // method removes the element from the DOM. Объект остаётся в коллекции routeListLi? Похоже, да, хотя не должен? Тогда он будет убит сборщиком мусора только после смерти routeListLi break; }; }; }); }); break; case 'MOB': // человек за бортом if(!map.hasLayer(mobMarker)) MOBalarm(); else if(!map.hasLayer(cursor)) centerMarkOn(); // включить крестик в середине break; } }); sidebar.on("closing", function(){ if(CurrnoFollowToCursor !== 1) noFollowToCursor = CurrnoFollowToCursor; // восстановим признак следования за курсором CurrnoFollowToCursor = 1; centerMarkOff(); // выключить крестик посередине if(currentRoute && delShapes()) editorEnabled='maybe'; // есть редактируемые слои else { editorEnabled=false; // если нет редактируемых слоёв -- запретим включать редактирования currentRoute = null; routeSaveName.value = ''; routeSaveDescr.value = ''; } }); // Сокрытие всего hideControlsToggler(hideControlsSwitch); // создаёт как-бы control, делающий невидимыми все control, перечисленные в controlsList // end controls // Поведение карты map.on('movestart zoomstart', function(event) { // карту начали двигать руками // функция отменяет следование карты за курсором, и устанавливает таймер, чтобы вернуть // пытается отделить собственные движения карты от юзерских, включая изменение масштаба if(userMoveMap) { // Убран флаг в куске, двигающем карту за курсором //alert('Карту сдвинули событием '+event.type); if(event.type == 'zoomstart') userMoveMap = 2; // юзер нажал zoom else { if(userMoveMap == 2) userMoveMap = true; // на это дело сработало movestart - игнорируем else { followToCursor=false; // запретим следование за курсором clearTimeout(followPaused); // отменим то, что есть followPaused = setTimeout('followToCursor=true;',followPause); // через время followPause разрешим обратно } } } }); map.on('zoomend', function(event) { if(distCirclesSwitch.checked) distCirclesUpdate(distCircles); // нарисуем круги дистанции if(map.hasLayer(centerMark)) centerMarkUpdate(); // нарисуем круги дистанции крестика в центре }); map.on('moveend', function(event) { // кластеризация точек POI, показывает кластеры в области просмотра let zoom = map.getZoom(); let pos = map.getCenter(); // Если не было зумирования и центр сдвинулся мало - не будем перепоказывать supercluster if((zoom == lastSuperClusterUpdatePosition[1]) && (map.distance(pos,lastSuperClusterUpdatePosition[0]) <= superclusterRadius*(40075016.686 * Math.abs(Math.cos(pos.lat*(Math.PI/180))))/Math.pow(2, zoom+8))) return; updateClasters(); lastSuperClusterUpdatePosition[0] = pos; lastSuperClusterUpdatePosition[1] = zoom; }); //map.on("layeradd", function(event) { //}); // Восстановим слои //var layers = JSON.parse(getCookie('GaladrielMaps')); // getCookie from galadrielmap.js var layers = storageHandler.restore('layers'); // storageHandler from galadrielmap.js // Занесём слои на карту if(layers) layers.reverse().forEach(function(layerid){ // потому что они там были для красоты последним слоем вверх for (var i = 0; i < mapList.children.length; i++) { // для каждого потомка списка mapList if (mapList.children[i].id==layerid) { // selectMap(mapList.children[i]); break; } } }); else { for (var i = 0; i < mapList.children.length; i++) { // для каждого потомка списка mapList if (mapList.children[i].id==defaultMap) { // найдём, который из них defaultMap selectMap(mapList.children[i]); // и покажкм его break; } } } // Рисование маршрута dravingLines.addTo(map); doRestoreMeasuredPaths(); // восстановим из кук сохранённые на устройстве маршруты routeControlsDeSelect(); // сделать кнопки рисования невыбранными var pointIcon = L.icon({ iconUrl: 'leaflet-omnivorePATCHED/symbols/point.png', iconSize: [32, 37], iconAnchor: [16, 37], tooltipAnchor: [16,-25], className: 'wpIcon', }); var anchorIcon = L.icon({ iconUrl: 'leaflet-omnivorePATCHED/symbols/anchor.png', iconSize: [32, 37], iconAnchor: [16, 37], tooltipAnchor: [16,-25], className: 'wpIcon' }); var cautionIcon = L.icon({ iconUrl: 'leaflet-omnivorePATCHED/symbols/caution.png', iconSize: [32, 37], iconAnchor: [16, 37], tooltipAnchor: [16,-25], className: 'wpIcon' }); /* map.on('editable:editing', // обязательный обработчик для editable для перересовывания расстояний при изменении пути function (e) { //console.log('обязательный обработчик для editable start by editable:editing',e); // А это норм, что оно глобально? if (e.layer instanceof L.Path) e.layer.updateMeasurements(); } ); */ map.on('editable:drawing:end', function(event) { // выключать кнопку "Начать" при окончании рисования, сделать доступной "Продолжить" //console.log('map.on [editable:drawing:end] event.target:',event.target); /* if(event.layer instanceof L.Marker){ console.log('[map.on editable:drawing:end] event.layer is a L.marker'); } */ if(event.layer instanceof L.Path){ //console.log('[map.on editable:drawing:end] event.layer is a L.Path'); routeContinueButton.disabled=false; } routeCreateButton.checked=false; }); map.on('editable:vertex:dragstart', function(event) { // Придурки из Mozilla сделали Error при вызове этой функции из десктопного браузера, когда как // в https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate ясно сказано: // "If the device doesn't support vibration, this method has no effect. " // И до какого-то момента так всё и было. // Короче, они это отключили, но никому не сказали: https://github.com/mdn/content/issues/34703 if(typeof window.navigator.vibrate === 'function') window.navigator.vibrate(200); // Вибрировать 200ms }); // Круги дистанции var centerMarkCircles = []; for (let n=0; n<4; n++) { centerMarkCircles.push( L.circle([], { color: '#FD00DB', weight: 1, opacity: 0.3, fill: false, pane: 'overlayPane', zIndexOffset: -503 })); distCircles.push(L.circle([0,0], { // указать координаты необходимо, потому что Leaflet обламывается при добавлении круга в мультислой, если у круга нет координат color: '#FD00DB', weight: 1, opacity: 0.3, fill: false, pane: 'overlayPane', zIndexOffset: -503 })); }; // центр экрана let centerMarkIcon = new L.divIcon({ className: "centerMarkIcon" // galadrielmap.css Установить прозрачность фона иначе, чем внешним стилем не удаётся }); var centerMarkMarker = L.marker(map.getBounds().getCenter(), { 'icon': centerMarkIcon, pane: 'overlayPane', // расположим маркер над тайлами, но ниже всего остального zIndexOffset: -1000 }); var centerMark = L.layerGroup([centerMarkMarker]); centerMarkCircles.forEach(circle => circle.addTo(centerMark)); // Символ ветра let windSymbolIcon = L.divIcon({ className: "", // если не указать className, то для L.divIcon будет рисоваться какой-то квадратик, и от него никак не избавиться. Глюк? iconAnchor: [-30,0], html:` <svg version="1.1" id="wSVGimage" width="135" height="30" transform="scale(1,1)" xmlns="http://www.w3.org/2000/svg"> <defs> <line id="bLine" x1="0" y1="2.5" x2="70" y2="2.5" /> <line id="w2.5" x1="3" y1="2.5" x2="10" y2="13" /> <polyline id="w5" points="0,2.5 10,2.5 25,25" fill="none"/> <g id="w25"> <polygon points="10,5 22.5,25 34.5,5" stroke-width="0" /> <line x1="0" y1="2.5" x2="34.5" y2="2.5" /> </g> </defs> <g id="wMark" fill="#8900FF" fill-opacity="0.75" stroke="#8900FF" stroke-width="5" stroke-opacity="0.75" > </g> </svg> ` }); let windSymbolMarker = L.marker([],{ icon: windSymbolIcon, pane: 'overlayPane', // расположим маркер над тайлами, но ниже всего остального zIndexOffset: -400 }); // Местоположение // маркеры var GpsCursor = L.icon({ iconUrl: './img/gpscursor.png', iconSize: [120, 120], // size of the icon iconAnchor: [60, 60], // point of the icon which will correspond to marker's location }); // курсор var NoGpsCursor = L.icon({ // этот значёк может показываться и при пропаже связи с сервером, а в этом случае загрузить картинку не удастся. Попытка загрузить её заранее не получилась: Leaflet, видимо, убивает долго неиспользуемые объекты. Или сборщик мусора? iconUrl: './img/gpscursor.png', iconSize: [120, 120], // size of the icon iconAnchor: [60, 60], // point of the icon which will correspond to marker's location className: "NoGpsCursorIcon" // galadrielmap.css }); var velocityCursor = L.icon({ iconUrl: './img/1x1.png', //iconUrl: './img/minLine.svg', }); var NoCursor = L.icon({ iconUrl: './img/1x1.png', iconSize: [0, 0], // size of the icon }); var cursor = L.marker(startCenter, { icon: GpsCursor, rotationAngle: 0, // начальный угол поворота маркера rotationOrigin: "50% 50%", // вертим маркер вокруг центра pane: 'overlayPane', // расположим маркер над тайлами, но ниже всего остального zIndexOffset: -500 }); // указатель скорости var velocityVector = L.marker(cursor.getLatLng(), { 'icon': velocityCursor, rotationAngle: 0, // начальный угол поворота маркера opacity: 0.1, pane: 'overlayPane', // расположим маркер над тайлами, но ниже всего остального zIndexOffset: -501 }); velocityVectorLengthInMnDisplay.innerHTML = velocityVectorLengthInMn; // нарисуем цену вектора скорости на панели управления // Точность ГПС var GNSScircle = L.circle(cursor.getLatLng(), { 'radius': 10, 'color':'#000000', 'weight':0, 'opacity':0.1, 'fillOpacity':0.1, pane: 'overlayPane', // расположим маркер над тайлами, но ниже всего остального zIndexOffset: -502 }); // Курсор: объединение всех фигур var positionCursor = L.layerGroup([GNSScircle,velocityVector,cursor]); distCirclesToggler(); // (если) добавим круги в курсор и заодно освежим куку windSwitchToggler(); // (если) добавим символ ветра в курсор и заодно освежим куку // Для визуализации collisionDetector var collisionIcon = L.icon({ iconUrl: './img/redbulletdot.svg', iconSize: [60, 60], iconAnchor: [30, 30] }); var collisionDirectionIcon = L.icon({ iconUrl: './img/redArrow.svg', iconSize: [20,24], iconAnchor: [10,30] }); var collisisonDetected = L.layerGroup(); // слой, на котором рисуются значки возможных столкновений collisionDetector var collisionDirectionsCursor = L.layerGroup(); // слой с указателями направлений на опасности столкновений /////////////////////////// collisionDetector test /////////////////////////////// //var collisisonAreas = L.layerGroup(); // для тестовых целей collisionDetector /////////////////////////// end collisionDetector test /////////////////////////////// // MOB marker var mobIcon = L.icon({ // iconUrl: mob_markerImg, // options.js //iconUrl: "img/mob.png", iconSize: [32, 37], //iconSize: [64, 74], iconAnchor: [16, 37], //iconAnchor: [32, 74], tooltipAnchor: [16,-25], className: 'mobIcon' }); // линия между положением и указанным маркером MOB var toMOBline = L.polyline([], { color: 'red', weight: 10, opacity:0.3, }) // восстановим маркеры //var mobMarker = getCookie('GaladrielMapMOB'); // getCookie from galadrielmap.js var mobMarker = storageHandler.restore('mobMarker'); // storageHandler from galadrielmap.js //console.log('Восстановление MOB: mobMarker',mobMarker); if(mobMarker) { let mobMarkerJSON; try{ mobMarkerJSON = JSON.parse(mobMarker); } catch(err){ console.log('saved MOB is bad, JSON.parse error:',err.text); }; //console.log('from cookie:',JSON.stringify(mobMarkerJSON)); // L.geoJSON не сохраняет левые поля из geoJSON if((typeof mobMarkerJSON === "object") && mobMarkerJSON.features && (typeof mobMarkerJSON.features === "object")){ let pt = false for(let feature of mobMarkerJSON.features){ if(feature.geometry.type != 'Point') continue; if((typeof feature.geometry.coordinates === "object") && feature.geometry.coordinates.length != 0){ pt=true; break; }; }; if(pt){ // В куке - объект geojson с точкой с координатами createMOBpointMarker(mobMarkerJSON);// Восстановим мультислой маркеров из GeoJSON //console.log('mobMarker from cookie:',mobMarker); }; }; }; // Если маркера MOB таки нет - создадим его. if((typeof mobMarker !== "object") || !(mobMarker instanceof L.LayerGroup)) { mobMarker = L.layerGroup().addLayer(toMOBline); // таким образом, mobMarker всегда есть. mobMarker.feature = {properties: {}}; }; // Позиционирование // Realtime периодическое обновление var spatialWebSocket; // var TPVdata = {}; // буфер с данными позиционирования, скорости и направления var lastDataUpdate=0; // момент последнего получения данных, в милисекундах var PosFreshBeforeMultiplexor=30; // через сколько интервалов PosFreshBefore убирать курсор совсем var lastPositionUpdate=0; // момент последнего обновления координат, в милисекундах function spatialWebSocketStart(){ // let checkDataFreshInterval; // объект периодического запуска проверки свежести данных spatialWebSocket = new WebSocket(`ws://${document.location.host}/signalk/v1/stream?subscribe=none`); // подписываться будем отдельно, по событию websocketOnOpen spatialWebSocket.onopen = function(event) { console.log("[spatialWebSocket open] Connection established"); // Если сервера не было, то списки могли изменится после его перезапуска // не уверен, что стоит заморачиваться с перегрузкой всех списков, перепоказом маршрутов и вот этим всем... //console.log("[spatialWebSocket open] Track list and route list refreshing"); //listPopulate(routeList,routeDirURI,false,restoreDisplayedRoutes); // список маршрутов, асинхронно //listPopulate(trackList,trackDirURI,true,function(){chkDisplayedList(trackList,trackDisplayed);}); // список путей, показывать текущий, асинхронно TPVsubscribe.subscribe.forEach(subscribe => subscribe.minPeriod = minWATCHinterval); // signalKsubscribe в options.js event.target.send(JSON.stringify(TPVsubscribe)); // подписываемся на получение данных console.log("[spatialWebSocket open] Subscribe sended"); // Хоть какая-то проверка актуальности координат -- за отсутствием чего-то такого в SignalK if(useSystemTimeouts){ // options.js // Попытка сделать минимально правильно в этом кривом SignalK // Хотя, в принципе, нужно пытаться получить .meta.timeout для каждой величины. // Во-первых, meta.timeout реально вообще нигде нет. По идее, его должен указывать производитель данных, но нет. // Во-вторых, к этому моменту может вообще не быть navigation, а по подписке не присылается meta // В-третьих, в доке указано, что должен быть meta.timeout, но если смотреть по аналогии с units, скажем, то оно meta.properties.longitude.timeout Как оно правильно -- неизвестно let timeout = getSelfPathC('navigation.position.meta.timeout'); if(timeout) PosFreshBefore = timeout*1000; timeout = getSelfPathC(ConfigDepthProp+'.meta.timeout'); if(timeout) { SpeedFreshBefore = timeout*1000; DepthFreshBefore = timeout*1000; WindFreshBefore = timeout*1000; } } // Попытка переподключения к серверу, если позиция давно не обновлялась checkDataFreshInterval = setInterval(function (){ //console.log('[checkDataFreshInterval] PosFreshBefore=',PosFreshBefore,'lastDataUpdate=',Date.now()-lastDataUpdate); if((Date.now()-lastDataUpdate)>PosFreshBefore){ console.log('The latest TPV data was received too long ago, trying to reconnect for checking.'); spatialWebSocket.close(1000,'The latest data was received too long ago'); } },PosFreshBefore); windSwitchToggler(); // (если) добавим символ ветра в курсор }; // end spatialWebSocket.onopen spatialWebSocket.onmessage = function(event) { //console.log(`[message] Данные TPV получены с сервера: ${event.data}`); let data; try{ data = JSON.parse(event.data); } catch(error){ console.log('spatialWebSocket: Parsing inbound data',error.message); return; } //console.log('From SignalK',data); if(!data.updates){ // ответ на какой-то запрос //console.log('Other From SignalK',data); if(data.self){ // handshaiking //vesselSelf = data.self.substring(8); // отрезано vessels. vesselSelf = data.self; } return; } for(let update of data.updates){ //console.log('update:',update); if(!update.values) continue; // непонятно, почему может не быть values, но иногда их нет for(let value of update.values){ // какое-то обновление данных пришло. //console.log('[spatialWebSocket.onmessage] recieved data:',data); lastDataUpdate = Date.now(); switch(value.path){ case 'navigation.position': TPVdata.lon = value.value.longitude; TPVdata.lat = value.value.latitude; //console.log('TPVdata lon lat',TPVdata.lon,TPVdata.lat); // timestamp будет один на все величины. Это неправильно, но, насколько я понимаю, // SignalK по крайней мере, для величин, не имеющих своего timestamp в источнике, // в качестве timestamp ставит текущее время. TPVdata.positionTime = update.timestamp; // там не требуется unix timestamp, а, наоборот, дата в виде строки break; case 'navigation.courseOverGroundTrue': TPVdata.track = value.value*180/Math.PI; break; case 'navigation.headingTrue': TPVdata.heading = value.value*180/Math.PI; TPVdata.headingTime = update.timestamp; break; // в принципе, может быть и navigation.headingMagnetic и navigation.headingCompass // причём разные (с учётом navigation.magneticDeviation) // Кто им доктор? case 'navigation.headingMagnetic': TPVdata.mheading = value.value*180/Math.PI; TPVdata.mheadingTime = update.timestamp; case 'navigation.headingCompass': TPVdata.mheading = value.value*180/Math.PI; if(TPVdata.magdev !== undefined) TPVdata.mheading += TPVdata.magdev; TPVdata.mheadin