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.

2,026 lines 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:1rem;margin:0 0.7rem 0 0;float:right;"><img src="img/no.svg" title="Clear" alt="Clear" width="8px" style="vertical-align:top;"></button>
					<button onClick='goToPositionField.value += "°";goToPositionField.focus();' style="width:2rem;height:1rem;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:1rem;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:1rem;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" title="Ok" alt="Ok" width="16px"></button>
				<button onClick='routeSaveName.value=""; routeSaveDescr.value="";' type='reset' class='okButton' style="float:left;"><img src="img/no.svg" title="<?php echo $clearTXT;?>" alt="<?php echo $clearTXT;?>" 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();
			MOBtabHighLight(true);	// Подсветим кнопку
		}
		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");
		//console.log("[spatialWebSocket open] Subscribed to",TPVsubscribe);
		// Хоть какая-то проверка актуальности координат -- за отсутствием чего-то такого в 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 value:',value);
				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;
					//console.log('[spatialWebSocket.onmessage] headingTrue=',TPVdata.heading)
					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.mheadingTime = update.timestamp;
					break;
				case 'navigation.speedOverGround':	// что именно -- устанавливается в конфигурации плагина
				case 'navigation.speedThroughWater':
					TPVdata.speed = value.value;
					TPVdata.speedTime = update.timestamp;
					break;
				case 'environment.depth.belowSurface':	// что именно -- устанавливается в конфигурации плагина
				case 'environment.depth.belowKeel':
				case 'environment.depth.belowTransducer':
					TPVdata.depth = value.value;
					TPVdata.depthTime = update.timestamp;
					break;
				case 'navigation.magneticVariation':	// The magnetic variation (declination) at the current position that must be added to the magnetic heading to derive the true heading. Easterly variations are positive and Westerly variations are negative (in Radians). магнитное склонение
					TPVdata.magvar = value.value*180/Math.PI;
					break;
				case 'navigation.magneticDeviation':	// Magnetic deviation of the compass at the current headingCompass погрешность компаса, магнитная девиация
					TPVdata.magdev = value.value*180/Math.PI;
					break;
				// Ветер. Будем определять его противоестественно: не относительно судна, а
				// относительно N
				case 'environment.wind.directionTrue':	// Абсолютный ветер. The wind direction relative to true north
					TPVdata.wanglet = value.value*180/Math.PI;
					TPVdata.windTime = update.timestamp;
					break;
				case 'environment.wind.angleApparent':	// Вымпельный ветер. Apparent wind angle, negative to port
					//console.log('environment.wind.angleApparent=',value.value*180/Math.PI);
					if(TPVdata.heading){	// приведём к абсолютному значению в соответствии с курсом
						//console.log('TPVdata.heading=',TPVdata.heading);
						TPVdata.wangler = TPVdata.heading+value.value*180/Math.PI;
						if(TPVdata.wangler >= 360) TPVdata.wangler -= 360;
						else if(TPVdata.wangler<0) TPVdata.wangler += 360;
						TPVdata.windTime = update.timestamp;
					}
					else if(TPVdata.track){	// но если вдруг курса нет, то к тому, что есть
						TPVdata.wangler = TPVdata.track+value.value*180/Math.PI;
						if(TPVdata.wangler >= 360) TPVdata.wangler -= 360;
						else if(TPVdata.wangler<0) TPVdata.wangler += 360;
						TPVdata.windTime = update.timestamp;
					};
					//console.log('TPVdata.wangler=',TPVdata.wangler);
					break;
				case 'environment.wind.angleTrueWater':	// Истинный ветер. True wind angle based on speed through water, negative to port
					if(TPVdata.heading){	// истинный ветер, но приведём к курсу
						TPVdata.wanglet = TPVdata.heading+value.value*180/Math.PI;
						if(TPVdata.wanglet >= 360) TPVdata.wanglet -= 360;
						else if(TPVdata.wanglet<0) TPVdata.wanglet += 360;
						TPVdata.windTime = update.timestamp;
					}
					else if(TPVdata.track){	// но если вдруг курса нет, то к тому, что есть
						TPVdata.wanglet = TPVdata.track+value.value*180/Math.PI;
						if(TPVdata.wanglet >= 360) TPVdata.wanglet -= 360;
						else if(TPVdata.wanglet<0) TPVdata.wanglet += 360;
						TPVdata.windTime = update.timestamp;
					};
					break;
				case 'environment.wind.angleTrueGround':	// Истинный ветер. True wind angle based on speed over ground, negative to port
					if(TPVdata.track){	// истинный ветер, приведём к курсовому углу
						TPVdata.wanglet = TPVdata.track+value.value*180/Math.PI;
						if(TPVdata.wanglet >= 360) TPVdata.wanglet -= 360;
						else if(TPVdata.wanglet<0) TPVdata.wanglet += 360;
						TPVdata.windTime = update.timestamp;
					}
					else if(TPVdata.heading){	// но если вдруг курсового угла нет, то к тому, что есть
						TPVdata.wanglet = TPVdata.heading+value.value*180/Math.PI;
						if(TPVdata.wanglet >= 360) TPVdata.wanglet -= 360;
						else if(TPVdata.wanglet<0) TPVdata.wanglet += 360;
						TPVdata.windTime = update.timestamp;
					};
					break;
				case 'environment.wind.speedTrue':	// Wind speed over water (as calculated from speedApparent and vessel's speed through water)
					TPVdata.wspeedt = value.value;
					TPVdata.windTime = update.timestamp;
					break;
				case 'environment.wind.speedApparent':	//  Apparent wind speed
					TPVdata.wspeedr = value.value;
					TPVdata.windTime = update.timestamp;
					break;
				}
			}
		}
		data = null;	// хотя это, по-видимому, не освободит память.
		realtimeTPVupdate(TPVdata);	// показываем новое каждый тик
		
		/////////////////////////// collisionDetector test ///////////////////////////////
		//displayCollisionAreas();	// показываем отладочную информацию предупреждения о столкновениях
		/////////////////////////// end collisionDetector test ///////////////////////////////
		
	}; // end spatialWebSocket.onmessage

	spatialWebSocket.onclose = function(event) {
		console.log(`spatialWebSocket closed: connection broken with code ${event.code} by reason ${event.reason}`);
		//console.log('lastDataUpdate',lastDataUpdate,Date.now()-lastDataUpdate,'PosFreshBefore*PosFreshBeforeMultiplexor',PosFreshBefore*PosFreshBeforeMultiplexor);
		if((Date.now()-lastDataUpdate)>PosFreshBefore*PosFreshBeforeMultiplexor) {
			positionCursor.remove(); 	// уберём курсор (layerGroup) с карты
			clearInterval(checkDataFreshInterval);	// остановить периодическую проверку свежести
		}
		else {
			cursor.setIcon(NoGpsCursor);	// заменим курсор (значёк) на серый
			positionTimeDisplay.innerHTML = '<blink>&infin;</blink>';	// но моргать оно не будет, потому что чтобы разные придурки не моргали где ни попадя, этот тег запретили всем совсем навсегда. Ибо нефиг!
		}
		velocityDial.innerHTML = '&nbsp;'; 	// обнулим панель приборов
		courseDisplay.innerHTML = '&nbsp;';
		locationDisplay.innerHTML = '&nbsp;';
		depthDial.innerHTML = '';
		// Уберём символ ветра
		windSymbolMarker.removeFrom(positionCursor);

		//MOBtab.className='disabled'; 	// если нет курсора (координат) -- невозможно включить режим MOB. Это плохая идея.
		window.setTimeout(spatialWebSocketStart, 3000); 	// перезапустим сокет через  секунд. В каком контексте здесь вызывается callback -- мне осталось непонятным, поэтому сокет ваще глобален
	}; // end spatialWebSocket.onclose

	spatialWebSocket.onerror = function(error) {
	  console.log(`[spatialWebSocket error] ${error.message}`);
	}; // end spatialWebSocket.onerror

	function realtimeTPVupdate(gpsdData) {
		//console.log('index.html [realtimeTPVupdate] gpsdData',gpsdData);
		// Положение неизвестно
		if(gpsdData.error || (gpsdData.lon == null)||(gpsdData.lat == null) || (gpsdData.lon == undefined)||(gpsdData.lat == undefined)) { 	// 
			console.log('No spatial info in GPSD data',gpsdData);
			//console.log('lastPositionUpdate=',lastPositionUpdate,'PosFreshBefore*PosFreshBeforeMultiplexor=',PosFreshBefore*PosFreshBeforeMultiplexor,Date.now()-lastPositionUpdate);
			if((Date.now()-lastPositionUpdate)>PosFreshBefore*PosFreshBeforeMultiplexor) {	// обычно PosFreshBefore -- 3-5 секунд
				positionCursor.remove(); 	// уберём курсор с карты
				//velocityDial.innerHTML = '&nbsp;'; 	// обнулим панель приборов
				//courseDisplay.innerHTML = '&nbsp;';
				//locationDisplay.innerHTML = '&nbsp;';
				//depthDial.innerHTML = '';
				//MOBtab.className='disabled'; 	// если нет курсора (координат) -- невозможно включить режим MOB. Это плохая идея.
				return;	// могут быть другие величины, например - глубина
			}
			else  cursor.setIcon(NoGpsCursor)	// заменим курсор (значёк) на серый
		}
		else {
			// Свежее ли положение известно? -- установим курсор в положение
			lastPositionUpdate = Date.now();
			//MOBtab.className=''; 	// координаты появились -- можно включить режим MOB
			positionCursor.invoke('setLatLng',[gpsdData.lat,gpsdData.lon]); // установим координаты всех маркеров
			if(!map.hasLayer(positionCursor)) positionCursor.addTo(map); 	// добавить курсор на карту
		};
		
		if(distCirclesSwitch.checked) distCirclesUpdate(distCircles);	// нарисуем круги дистанции
		let positionTime = new Date(gpsdData.positionTime);
		let now = new Date();
		//console.log('now-positionTime=',(now - new Date(gpsdData.positionTime))/1000);
		if((now - positionTime) > PosFreshBefore) {
			cursor.setIcon(NoGpsCursor); 	// свежее положение было определено раньше, чем PosFreshBefore милисекунд назад, серый курсор
		}
		else cursor.setIcon(GpsCursor);
		// Информация о свежести данных на приборной панели
		positionTimeDisplay.innerHTML = (Math.round((now-positionTime)/100)/10).toFixed(1);
		
		// Показ скорости и прочего
		//console.log('Index gpsdData',gpsdData.speed);
		// https://wiki.openstreetmap.org/wiki/Zoom_levels, тайл имеет размер 256 пикселей
		var metresPerPixel = (40075016.686 * Math.abs(Math.cos(cursor.getLatLng().lat*(Math.PI/180))))/Math.pow(2, map.getZoom()+8); 	// in WGS84
		if(gpsdData.speed==undefined || gpsdData.speed==null) {
			velocityDial.innerHTML = '&nbsp;';
			velocityVector.setIcon(NoCursor);
		}
		else {
			//var velocity = Math.round((gpsdData.speed*60*60/1000)*10)/10; 	// скорость от gpsd - в метрах в секунду
			var velocity = Math.round((gpsdData.speed*60*60/1000)*10)/10; 	// скорость от gpsd - в метрах в секунду

			// Установим длину указателя скорости за  минуты
			var velocityCursorLength = gpsdData.speed*60*velocityVectorLengthInMn; 	// метров  за  минуты
			velocityCursorLength = Math.round(velocityCursorLength/metresPerPixel);	// пикселей
			//console.log('map.getZoom='+map.getZoom()+'\nmetresPerPixel='+metresPerPixel+'\ngpsdData.speed='+gpsdData.speed+'\nvelocityCursorLength='+velocityCursorLength);
			velocityCursor.options.iconSize=[5,velocityCursorLength];
			velocityCursor.options.iconAnchor=[3,velocityCursorLength];
			velocityVector.setIcon(velocityCursor); 	// изменить иконку у маркера
			if((now - new Date(gpsdData.speedTime)) > SpeedFreshBefore) {	// тайм-штамп в SignalK один на посылку данных
				velocityDial.innerHTML = `<span style="color:silver;">${velocity}</span>`;
			}
			else velocityDial.innerHTML = velocity;
		}
		if(gpsdData.depth) {
			//console.log('gpsdData.depth:',gpsdData.depth);
			depthDial.innerHTML = '<br><br><div style="font-size:50%;">'+dashboardDepthMesTXT+'</div><br>';
			if((now - new Date(gpsdData.depthTime)) > DepthFreshBefore) {
				depthDial.innerHTML += '<div style="color:silver;">'+(Math.round(gpsdData.depth*100)/100)+'</div>';
			}
			else depthDial.innerHTML += '<div>'+(Math.round(gpsdData.depth*100)/100)+'</div>';
			depthDial.innerHTML += '<br><div style="font-size:50%;">'+dashboardMeterMesTXT+'</div>';
		}
		else {
			depthDial.innerHTML = '';
		}
		
		// Направление
		if((gpsdData.heading != undefined) && ((now - new Date(gpsdData.headingTime)) > PosFreshBefore)) {
			// На самом деле, это не очень надо, потому что обычно heading и track согласованы,
			// но может оказаться так, что heading было, а потом не стало. Тогда будет весело.
			delete gpsdData.heading;
			delete gpsdData.headingTime;
		}
		//console.log('[realtimeTPVupdate] gpsdData track',gpsdData.track,'heading',gpsdData.heading);
		if(gpsdData.track == null || gpsdData.track == undefined) {	// нет путевого угла
			if(gpsdData.heading !== undefined) {	// зато есть курс
				velocityVector.setRotationAngle(gpsdData.heading); // повернём оба маркера направления
				cursor.setRotationAngle(gpsdData.heading);
				courseDisplay.innerHTML = Math.round(gpsdData.heading); // покажем направление на приборной панели
				// Заменим подписи
				dashboardCourseTXT.innerHTML = i18n.stringsandliterals.dashboardHeadingTXT
				dashboardCourseAltTXT.innerHTML = i18n.stringsandliterals.dashboardHeadingAltTXT
			}
			else if(gpsdData.mheading !== undefined){	// или магнитный курс
				if(gpsdData.magvar !== undefined) {		// если есть склонение -- он истинный курс
					let heading = gpsdData.mheading + gpsdData.magvar;
					velocityVector.setRotationAngle(heading); // повернём оба маркера направления
					cursor.setRotationAngle(heading);
					courseDisplay.innerHTML = Math.round(heading); // покажем направление на приборной панели
					// Заменим подписи
					dashboardCourseTXT.innerHTML = i18n.stringsandliterals.dashboardHeadingTXT
					dashboardCourseAltTXT.innerHTML = i18n.stringsandliterals.dashboardHeadingAltTXT
				}
				else {
					velocityVector.setRotationAngle(gpsdData.mheading); // повернём оба маркера направления
					cursor.setRotationAngle(gpsdData.mheading);
					courseDisplay.innerHTML = Math.round(gpsdData.mheading); // покажем направление на приборной панели
					// Заменим подписи
					dashboardCourseTXT.innerHTML = i18n.stringsandliterals.dashboardMHeadingTXT
					dashboardCourseAltTXT.innerHTML = i18n.stringsandliterals.dashboardMHeadingAltTXT
				}
			}
			else {	// нет никакой информации о направлении
				courseDisplay.innerHTML = '&nbsp;';
				velocityVector.setRotationAngle(0); // повернём оба маркера направления
				cursor.setRotationAngle(0);
			}
		}
		else {
			velocityVector.setRotationAngle(gpsdData.track);
			if(gpsdData.heading !== undefined) cursor.setRotationAngle(gpsdData.heading);
			else if((gpsdData.mheading !== undefined) && (gpsdData.magvar !== undefined)) cursor.setRotationAngle(gpsdData.mheading + gpsdData.magvar);
			else cursor.setRotationAngle(gpsdData.track);
			if((now-positionTime) > PosFreshBefore) {	// положение протухло
				courseDisplay.innerHTML = '<span style="color:silver;">'+Math.round(gpsdData.track)+'</span>'; // покажем направление на приборной панели
			}
			else courseDisplay.innerHTML = Math.round(gpsdData.track); // покажем направление на приборной панели
			// Заменим подписи, вдруг до этого не было путевого угла
			dashboardCourseTXT.innerHTML = i18n.stringsandliterals.dashboardCourseTXT
			dashboardCourseAltTXT.innerHTML = i18n.stringsandliterals.dashboardCourseAltTXT
		}
		// Символ ветра
		//console.log('wSVGimage:',document.getElementById('wSVGimage'));
		if((now - new Date(gpsdData.windTime)) > WindFreshBefore) {
			if(TPVdata.wspeedt) delete TPVdata.wspeedt;
			if(TPVdata.wanglet) delete TPVdata.wanglet;
			if(TPVdata.wspeedr) delete TPVdata.wspeedr;
			if(TPVdata.wangler) delete TPVdata.wangler;
			windSymbolUpdate();
		};
		if(windSwitch.checked){
			if(((TPVdata.wspeedr != undefined) && (TPVdata.wangler != undefined)) || ((TPVdata.wspeedt != undefined) && (TPVdata.wanglet != undefined))){
				windSymbolUpdate();
			}
		}
		/*
		// Визуализация collisionDetector
		// оказывается, размеры ниже устанавливаются не в смысле значка, а как прямоугольник
		// на экране, параллельный осям. Поэтому перед изменением размеров надо
		// повернуть значёк в исходное положение
		collisionAreaMarker.setRotationAngle(0); 
		collisionAreaIcon.options.iconSize=[velocityCursorLength/10,velocityCursorLength];
		collisionAreaIcon.options.iconAnchor=[velocityCursorLength/20,velocityCursorLength];
		collisionAreaMarker.setIcon(collisionAreaIcon);
		collisionAreaMarker.setLatLng(cursor.getLatLng() );// положение
		collisionAreaMarker.setRotationAngle(gpsdData.heading); // повернём маркер
		*/
		
		// Окружность точност ГПС
		var errGNSS = (+gpsdData.errX+gpsdData.errY)/2;
		if(!errGNSS) errGNSS = 10; // метров
		if(errGNSS/metresPerPixel > 15) GNSScircle.setRadius(errGNSS); 	// кружок точности больше кружка курсора
		else GNSScircle.setRadius(0);

		// Карту в положение курсора
		//console.log("[spatialWebSocket.onmessage] followToCursor=", followToCursor);
		if(followToCursor && !noFollowToCursor) { 	// если сказано следовать курсору, и это разрешено глобально
			userMoveMap = false;
			map.setView(cursor.getLatLng()); // подвинем карту на позицию маркера. Почему-то этот процесс вызывает перерисовывание (всех?) видимых полилиний. Видимо, это нормально?
			userMoveMap = true;
		}

		// координаты курсора с точностью знаков
		lat = Math.round(cursor.getLatLng().lat*10000)/10000; 	 	// широта
		lng = Math.round(cursor.getLatLng().lng*10000)/10000; 	 	// долгота
		if((now-positionTime) > PosFreshBefore) {	// положение протухло
			locationDisplay.innerHTML = '<span style="color:silver;">'+latTXT+' '+lat+'<br>'+longTXT+' '+lng+'</span>';	
		}
		else locationDisplay.innerHTML = latTXT+' '+lat+'<br>'+longTXT+' '+lng;	
		followSwitch.checked = !noFollowToCursor; 	// выставим переключатель на панели Настроек в текущее положение	
		
		// MOB
		if(mobMarker.hasLayer(currentMOBmarker) ){	// Если это чужой MOB, то currentMOBmarker может не показываться или быть не указан
			//console.log('[spatialWebSocket.onmessage] mobMarker.getLayers():',mobMarker.getLayers());
			if(currentMOBmarker){	// Если это чужой MOB, то currentMOBmarker может быть не указан
				let latlng1 = cursor.getLatLng();
				let latlng2 = currentMOBmarker.getLatLng();
				toMOBline.setLatLngs([latlng1,latlng2]); 	//console.log('обновим линию к текущему маркеру MOB');
				// информация о MOB на панели
				const azimuth = bearing(latlng1, latlng2);
				//console.log('azimuth=',azimuth,latlng1, latlng2)
				azimuthMOBdisplay.innerHTML = Math.round(azimuth);
				distanceMOBdisplay.innerHTML = Math.round(latlng1.distanceTo(latlng2));
				locationMOBdisplay.innerHTML = latTXT+' '+Math.round(currentMOBmarker.getLatLng().lat*10000)/10000+'<br>'+longTXT+' '+Math.round(currentMOBmarker.getLatLng().lng*10000)/10000;	
				let heading = gpsdData.track || gpsdData.heading || gpsdData.mheading;
				if(heading) {
					let relBearing = azimuth-heading+22.5;	// половина от 45 против часовой стрелке
					if(relBearing<0) relBearing = 360+relBearing;
					relBearing = Math.floor(relBearing/45); 	// курсовой угол (relative bearing) / 45 градусов -- номер сектора, против часовой стрелки
					if(relBearing>7) relBearing = 0;
					directionMOBdisplay.innerHTML = relBearingTXT[relBearing];
				};
			};
		}
	}; // end function realtimeTPVupdate
}; // end function spatialWebSocketStart

spatialWebSocketStart(); 	// запускам периодическую функцию получать TPV

function spatialWebSocketStop(message=''){
	console.log('Stop recieve TPV',);
	spatialWebSocket.close(1000,message);
} // end function spatialWebSocketStop


// Данные AIS
// 	Запуск периодических функций
var aisWebSocket;	// будет глобальный сокет для AIS

// буфер с данными AIS
// Но у нас уже есть vehicles? В vehicles -- показываемые объекты Leaflet,
// а здесь -- все сведения о целях AIS, показываемых сейчас, или нет.
// Если цель сперва протухнет, а потом снова объявится -- у нас уже есть вся статическая информация о ней.
var AISdata = {};	
function watchAISstart() {
	//console.log('AIS switched ON');
	let checkDataFreshInterval;	// объект периодического запуска проверки свежести данных

	aisWebSocket = new WebSocket(`ws://${document.location.host}/signalk/v1/stream?subscribe=none`); 	//  подписываться будем отдельно, по событию websocketOnOpen
	aisWebSocket.onopen = function(event) {
		console.log("[aisWebSocket open] Connection established");
		AISsubscribe.subscribe.forEach(subscribe => subscribe.minPeriod = minWATCHinterval);	//  AISsubscribe в options.js
		event.target.send(JSON.stringify(AISsubscribe));	// подписываемся на получение данных
		console.log("[aisWebSocket open] Subscribe sended");
		// Хоть какая-то проверка актуальности целей AIS -- за отсутствием чего-то такого в SignalK
		//console.log('PosFreshBefore=',PosFreshBefore);
		checkDataFreshInterval = setInterval(function (){
			for(const vehicle in vehicles){
				if((!vehicles[vehicle].options.timestamp) || ((Date.now()-vehicles[vehicle].options.timestamp*1000)<aisFreshBefore)) continue;
				//console.log(vehicle,'протух в vehicles',(Date.now()-vehicles[vehicle].options.timestamp*1000)/1000,aisFreshBefore/1000);
				vehicles[vehicle].remove();
				vehicles[vehicle] = null;
				delete vehicles[vehicle];
			}
		},PosFreshBefore);
	}; // end aisWebSocket.onopen

	aisWebSocket.onmessage = function(event) {
		//console.log(`[aisWebSocket message] Данные AIS получены с сервера: ${event.data}`);
		let data;
		try{
			data = JSON.parse(event.data);
		}
		catch(error){
			console.log('aisWebSocket: Parsing inbound data',error.message);
			return;
		}
		if(!data.updates){	// handshaiking
			return;
		}
		//console.log('data:',data);
		//console.log('data.context:',data.context,vesselSelf,AISdata);
		// Было бы неправильно показывать AIS только при наличии своих координат
		// но пока их нет -- нет и vesselSelf, а значит, среди целей AIS может быть и сам я
		if(data.context == vesselSelf) {
			if(vesselSelf in AISdata){
				delete AISdata[vesselSelf];
				vehicles[vesselSelf].remove();
				vehicles[vesselSelf] = null;
				delete vehicles[vesselSelf];
			}
			return;
		}
		// Приходит по одному сообщению на context, при этом - множество сообщений на цель AIS,
		// по мере прихода предложений AIS.
		// с, возможно, множеством updates
		//const vessel = data.context.substring(8);	// vessels.
		const vessel = data.context;	// возможно, так лучше, чтобы ключи были заведомо строки.
		if(!AISdata[vessel]) AISdata[vessel] = {};
		//if(AISdata[vessel].shipname == 'Sample Ship Name') console.log('Before',JSON.stringify(AISdata[vessel]));
		//AISdata[vessel].mmsi = data.context.split(':').pop();	
		//console.log('[aisWebSocket.onmessage] vessel=',vessel,data);
		for(let update of data.updates){
			for(let value of update.values){
				//console.log('[aisWebSocket.onmessage] data.context=',data.context,value.path,value.value);
				//console.log(data.context,value);
				//if(data.context == 'vessels.urn:mrn:imo:mmsi:201246757') console.log(data.context,value.path,value.value);
				switch(value.path){
				case '':	// похоже, правильный путь - "", а не name, но тогда нет места для mmsi, ибо оно тоже по пути "". 
							// Но, видимо, по одному пути может быть много объектов?
							// Короче говоря, фактически name и mmsi -- это свойства, находящиеся по пути ""
							// При этом в подписку по "" попадают и communication и registrations
							// Это тоже объекты в ""?
					//console.log(data.context,value.value);
					for(const key in value.value){
						switch(key){
						case 'name':
							AISdata[vessel].shipname = value.value[key];
							break;
						case 'mmsi':
							AISdata[vessel].mmsi = value.value[key];
							break;
						case 'registrations':
							AISdata[vessel].imo = value.value[key].imo;
							break;
						case 'communication':
							//console.log(data.context,key,value.value[key]);
							if(value.value[key].callsignVhf) AISdata[vessel].callsign = value.value[key].callsignVhf;
							if(value.value[key].netAIS) AISdata[vessel].netAIS = value.value[key].netAIS;
							break;
						}
					}
					break;
				case 'name':
					AISdata[vessel].shipname = value.name;
					break;
				case 'mmsi':
					AISdata[vessel].mmsi = value.mmsi;
					break;
				case 'registrations.imo':
					AISdata[vessel].imo = value.imo;
					break;
				case 'communication.callsignVhf':
					AISdata[vessel].callsign = value.value;
					break;
				case 'communication.netAIS':
					AISdata[vessel].netAIS = value.value;
					break;
				case 'design.aisShipType':
					AISdata[vessel].shiptype = value.value.id;
					AISdata[vessel].shiptype_text = value.value.name;
					break;
				case 'design.draft':
					if(!isNaN(value.value.maximum)) AISdata[vessel].draught = value.value.maximum;
					else if(!isNaN(value.value.current)) AISdata[vessel].draught = value.value.current;
					break;
				case 'design.length':
					if(!isNaN(value.value.overall)) AISdata[vessel].length = value.value.overall;
					break;
				case 'design.beam':
					if(!isNaN(value.value)) AISdata[vessel].beam = value.value;
					break;
				case 'navigation.position':
					AISdata[vessel].lon = value.value.longitude;
					AISdata[vessel].lat = value.value.latitude;
					//console.log(vessel,'navigationDatetime=',AISdata[vessel].navigationDatetime,`AISdata[vessel].timestamp=`,AISdata[vessel].timestamp,Math.round(Date.parse(update.timestamp)/1000)-AISdata[vessel].timestamp);
					if(!AISdata[vessel].navigationDatetime) AISdata[vessel].timestamp = Math.round(Date.parse(update.timestamp)/1000);	// Если navigation.datetime не посылается
					break;
				case 'navigation.safety_related_text':	
					AISdata[vessel].safety_related_text = value.value;
					break;
				case 'navigation.state_text':	
					AISdata[vessel].status_text = value.value;
					//console.log(data.context,value.path,value.value);
					break;
				case 'navigation.state':	
					// Злопротивный SignalK присылает статус как левый текст, причём не всегда,
					// а когда делает какие-то выводы относительно этого статуса.
					// Так, если исходное сообщение AIS имеет тип 14 и mmsi 97.. - то будет 
					// type ais-sart, а иначе - исходный тип 14. Казлы.
					//console.log(value.value);
					if(isNaN(value.value)) {
						if(!AISdata[vessel].status_text) AISdata[vessel].status_text = value.value;
						switch(value.value){	//Navigational status 0 = under way using engine, 1 = at anchor, 2 = not under command, 3 = restricted maneuverability, 4 = constrained by her draught, 5 = moored, 6 = aground, 7 = engaged in fishing, 8 = under way sailing, 9 = reserved for future amendment of navigational status for ships carrying DG, HS, or MP, or IMO hazard or pollutant category C, high speed craft (HSC), 10 = reserved for future amendment of navigational status for ships carrying dangerous goods (DG), harmful substances (HS) or marine pollutants (MP), or IMO hazard or pollutant category A, wing in ground (WIG);11 = power-driven vessel towing astern (regional use), 12 = power-driven vessel pushing ahead or towing alongside (regional use); 13 = reserved for future use, 14 = AIS-SART (active), MOB-AIS, EPIRB-AIS 15 = undefined = default (also used by AIS-SART, MOB-AIS and EPIRB-AIS under test)
						case 'motoring':
							AISdata[vessel].status = 0;	// under way using engine
							break;
						case 'anchored':
							AISdata[vessel].status = 1;	// at anchor
							break;
						case 'not under command':
							AISdata[vessel].status = 2;	// not under command
							break;
						case 'restricted manouverability':
							AISdata[vessel].status = 3;	// restricted maneuverability
							break;
						case 'constrained by draft':
							AISdata[vessel].status = 4;	// constrained by her draught
							break;
						case 'moored':
							AISdata[vessel].status = 5;	// moored
							break;
						case 'aground':
							AISdata[vessel].status = 6;	// aground
							break;
						case 'fishing':
							AISdata[vessel].status = 7;	// engaged in fishing
							break;
						case 'sailing':
							AISdata[vessel].status = 8;	// under way sailing
							break;
						case 'ais-sart':
							AISdata[vessel].status = 14;	// AIS-SART (active), MOB-AIS, EPIRB-AIS
							break;
						case 'default':
						default:
							AISdata[vessel].status = 15;	// undefined = default (also used by AIS-SART, MOB-AIS and EPIRB-AIS under test
						};
					}
					else AISdata[vessel].status = value.value;
					break;
				case 'navigation.courseOverGroundTrue':
					if(!isNaN(value.value)) AISdata[vessel].course = Math.round(value.value*180/Math.PI);
					break;
				case 'navigation.headingTrue':
					if(!isNaN(value.value)) AISdata[vessel].heading = Math.round(value.value*180/Math.PI);
					break;
				case 'navigation.speedOverGround':
					if(!isNaN(value.value)) AISdata[vessel].speed = value.value;
					break;
				case 'navigation.destination.commonName':
					if(value.value) AISdata[vessel].destination = value.value;
					break;
				case 'navigation.destination.eta':
					if(value.value) AISdata[vessel].eta = value.value;
					break;
				case 'navigation.datetime':
					const ts = Math.round(Date.parse(value.value)/1000);
					AISdata[vessel].timestamp = ts;
					// значение navigation.datetime, чтобы знать, что его получаем по подписке.
					// Если нет -- используется таймштамп координат. Но то, что SignalK не указывает
					// navigation.datetime для AIS -- странно. В сообщениях AIS есть время.
					AISdata[vessel].navigationDatetime = ts;	
					//console.log(vessel,'navigation.datetime=',value.value,AISdata[vessel].timestamp);
					break;
				};
			};
		};
		//console.log('[aisWebSocket.onmessage] vessel:',vessel,JSON.stringify(AISdata[vessel]));
		//if(AISdata[vessel].shipname == 'Sample Ship Name') {
			//console.log('After',JSON.stringify(AISdata[vessel]));
			//console.log('After',AISdata[vessel]);
			//console.log('[aisWebSocket.onmessage] AISdata:',AISdata);
		//}
		
		// это AIS MOB
		// данные приходят по мере потока AIS, и первым - mmsi
		// Похоже, иногда почему-то mmsi приходит не первым, даже после координат,
		// и тогда эта цель AIS будет показываться и как MOB, и как кораблик.
		// Поэтому выделяем mmsi из ключа
		if(!AISdata[vessel].mmsi) AISdata[vessel].mmsi = vessel.split(':').pop();	// последний элемент строки, mmsi
		if(AISasMOB && AISdata[vessel].mmsi && (AISdata[vessel].mmsi.startsWith('972') || AISdata[vessel].mmsi.startsWith('974'))){
		  	// Поскольку данные AIS MOB приходят всегда, изменилось там что-нибудь, или нет
		  	// следует оставить их в AISdata, но показывать не как кораблик, а как MOB
			//console.log('[aisWebSocket.onmessage] AIS MOB:',vessel,JSON.stringify(AISdata[vessel]));
		  	realtimeAIS_MOBupdate(vessel);	// рисует на карте значки MOB и поднимает (но не прекращает) режим MOB на сервере
		}
		else {
			//console.log('[aisWebSocket.onmessage] AIS:',vessel,JSON.stringify(AISdata[vessel]));
			realtimeAISupdate(vessel);	// рисуем на карте эту цель AIS
		};
		
		/////////////////////////// collisionDetector test ///////////////////////////////
		//displayCollisionAreas();	// показываем отладочную информацию предупреждения о столкновениях
		/////////////////////////// end collisionDetector test ///////////////////////////////
		
	}; // end aisWebSocket.onmessage

	aisWebSocket.onclose = function(event) {
		console.log(`aisWebSocket closed: connection broken with code ${event.code} by reason ${event.reason}`);
		if(DisplayAISswitch.checked ) window.setTimeout(watchAISstart, 3000); 	// перезапустим сокет через  секунд, если в интерфейсе указано
		for(const vehicle in vehicles){
			vehicles[vehicle].remove();	// удалить из DOM
			vehicles[vehicle] = null;
			delete vehicles[vehicle];
		}
		clearInterval(checkDataFreshInterval);	// остановить периодическую проверку свежести
	}; // end aisWebSocket.onclose

	aisWebSocket.onerror = function(error) {
	  console.log(`[aisWebSocket error] ${error.message}`);
	}; 	//end aisWebSocket.onerror

	function realtimeAISupdate(vehicle) {
	/* Это вызывается для каждой цели AIS для её показа */
	
	const defaultSymbol = [0.8,0, -0.3,0.35, -0.3,-0.35]; 	// треугольник вправо, расстояния от центра, через которые нарисуют polyline
	const noHeadingSymbol = [0.35,0, 0,0.35, -0.35,0, 0,-0.35]; 	// ромбик
	const netAISdefaultSymbol = [1*0.5,0, 0.25*0.5,0.25*0.5, 0,1*0.5, -0.25*0.5,0.5*0.5, -1*0.5,0.75*0.5, -1*0.5,-0.75*0.5, -0.25*0.5,-0.5*0.5, 0,-1*0.5, 0.25*0.5,-0.25*0.5]; 	// треугольник, расстояния от центра, через которые нарисуют polyline
	const netAISnoHeadingSymbol = [1*0.35,0, 0.75*0.35,0.5*0.35, 1*0.35,1*0.35, 0.5*0.35,0.75*0.35, 0,1*0.35, -0.5*0.35,0.75*0.35, -1*0.35,1*0.35, -0.75*0.35,0.5*0.35, -1*0.35,0, -0.75*0.35,-0.5*0.35, -1*0.35,-1*0.35, -0.5*0.35,-0.75*0.35, 0,-1*0.35, 0.5*0.35,-0.75*0.35, 1*0.35,-1*0.35, 0.75*0.35,-0.5*0.35]; 	// ромбик: правый, верхний, левый, нижний ПРотив часовой от правого?
	//console.log(vehicle,'протух на',(Date.now()-AISdata[vehicle].timestamp*1000)/1000,aisFreshBefore/1000);
	// не показываем протухшие цели. Они могут появляться быстрее, чем удаляются из списка aisData
	// тем более, что решили из AISdata их не удалять
	if(vehicles[vehicle] && (Date.now()-AISdata[vehicle].timestamp*1000)>aisFreshBefore){	
		//console.log('Протух vehicle',vehicle,vehicles[vehicle],AISdata[vehicle]);
		//delete AISdata[vehicle];
		vehicles[vehicle].remove();
		vehicles[vehicle] = null;
		delete vehicles[vehicle];
		return;	// ? Может быть, надо, оставив в AISdata, удалить из vehicles и с экрана?
	}
	if(!vehicles[vehicle]) { 	// global var, массив layers с целями
		//console.log('Create layers',vehicle,AISdata[vehicle]);
		vehicles[vehicle] = L.trackSymbol(L.latLng(0,0),{
			trackId: vehicle,
			leaderTime: velocityVectorLengthInMn*60,
			fill: true,
			fillOpacity: 1.0,
			stroke: true,
			opacity: 1.0,
			weight: 1.0,
		});
	};
	// Значёк будем переприсваивать каждый раз, потому что в SignalK неизвестно, когда придёт
	// признак AISdata[vehicle].netAIS
	if(AISdata[vehicle].netAIS) { 	// цель получена от netAIS
		//console.log('Create netAIS layers',vehicle,AISdata[vehicle]);
		vehicles[vehicle].setDefaultSymbol(netAISdefaultSymbol);
		vehicles[vehicle].setNoHeadingSymbol(netAISnoHeadingSymbol);
	}
	else { 	// цель получена от локального приёмника AIS
		vehicles[vehicle].setDefaultSymbol(defaultSymbol);
		vehicles[vehicle].setNoHeadingSymbol(noHeadingSymbol);
	}
	if(AISdata[vehicle].status==14)	AISdata[vehicle].sart=true;	// .sart отдельно потому, что непонятно, по каким признакам устаналивать, что это sart. В данном случае - только по признаку status==14
	else AISdata[vehicle].sart=false;
	// обновим данные Здесь обязательно нужно передать "по значению", почему -- неясно.	
	// addData просто заменяет всё на параметр? Оно заменяет всё на ссылку на параметр.
	// Но тогда всё равно данные должны быть полными, ведь в AISdata полные данные?
	//if(AISdata[vehicle].mmsi == '201246757') {
	//	// в AISdata действительно полные данные
	//	//console.log(JSON.stringify(AISdata[vehicle]));
	//	console.log(AISdata[vehicle]);
	//};
	// Видимо, проблема в addData и/или дальше, в L.trackSymbol. Нужно ли разбираться?
	vehicles[vehicle].addData(JSON.parse(JSON.stringify(AISdata[vehicle]))); 
	//vehicles[vehicle].addData(AISdata[vehicle]); 
	//console.log('[realtimeAISupdate] vehicles[vehicle]:',vehicles[vehicle]);
	
	// если сперва приходило всякое, но координат не было -- цели будут скапливаться в L.latLng(0,0)
	// поэтому добавим на карту, только если есть координаты.
	let latlng = vehicles[vehicle].getLatLng();
	if((latlng.lat || latlng.lng) && !map.hasLayer(vehicles[vehicle])){
		vehicles[vehicle].addTo(map);
		//console.log('[realtimeAISupdate] vehicles[vehicle]:',vehicle,vehicles[vehicle]);
	};
	} // end function realtimeAISupdate

	function realtimeAIS_MOBupdate(vessel) {
	/* Это вызывается для каждой цели AIS MOB для её показа */
	if((AISdata[vessel].lon === undefined) || (AISdata[vessel].lat === undefined)) return;
	//console.log('[realtimeAIS_MOBupdate] AIS MOB:',vessel,JSON.stringify(AISdata[vessel]));
	//console.log('[realtimeAIS_MOBupdate] timestamp: маркера пришло',mobMarker.feature.properties.timestamp,'<',AISdata[vessel].timestamp);
	if(!mobMarker.feature.properties.timestamp) mobMarker.feature.properties.timestamp = 0;
	if(mobMarker.feature.properties.timestamp < AISdata[vessel].timestamp){	// пришли свежие данные
		MOBalarm([AISdata[vessel].lat,AISdata[vessel].lon],AISdata[vessel]);
	};
	}; // end function realtimeAIS_MOBupdate

return aisWebSocket
} // end function watchAISstart


watchAISstart(); 	// запускам периодическую функцию смотреть AIS
DisplayAISswitch.checked = true;

function watchAISstop(message=''){
let res = false;
try {
	aisWebSocket.close(1000,message);
	console.log('AIS switched OFF');
	res = true;
}
catch(err) {};
// даже если неуспешно, всё равно
for(const vehicle in vehicles){	// уберём цели AIS с карты
	vehicles[vehicle].remove();
	vehicles[vehicle] = null;
	delete vehicles[vehicle];
};
collisionDirectionsCursor.clearLayers();	// очистим слой указателей направлений на опасности столкновений на курсоре
collisionDirectionsCursor.remove();
collisisonDetected.clearLayers();	// очистим слой 
collisisonDetected.remove();
return res;
} // end function watchAISstop

function watchAISswitching(){
let res;
if(DisplayAISswitch.checked) {
	res = watchAISstart();
	//console.log('[watchAISswitching] START res=',res,'DisplayAISswitch.checked=',DisplayAISswitch.checked,);
	if(!res) DisplayAISswitch.checked = false;
}
else {
	res = watchAISstop('Dispalying AIS stopped');
	//console.log('[watchAISswitching] STOP res=',res,'DisplayAISswitch.checked=',DisplayAISswitch.checked,);
	if(!res) DisplayAISswitch.checked = true;
}
}; // end function watchAISswitching

// Уведомления
var alarmWebSocket;
function alarmWebSocketStart(){
//
	alarmWebSocket = new WebSocket(`ws://${document.location.host}/signalk/v1/stream?subscribe=none`); 	//  подписываться будем отдельно, по событию websocketOnOpen
	alarmWebSocket.onopen = function(event) {
		console.log("[alarmWebSocket open] Connection established");
		event.target.send(JSON.stringify(notificationaSubscribe));	// подписываемся на получение данных
		if(map.hasLayer(mobMarker)){ 	// если показывается мультислой с маркерами MOB
			//console.log("[alarmWebSocket open] Имеется режим MOB - передадим его на сервер");
			sendMOBtoServer(); 	// отдадим данные MOB для передачи на сервер, на всякий случай -- вдруг там не знают
		}
		collisionDirectionsCursor.addTo(positionCursor);	// слой с указателями направлений на опасности столкновений
		console.log("[alarmWebSocket open] Subscribe sended");
	}; // end alarmWebSocket.onopen

	alarmWebSocket.onmessage = function(event) {
		//console.log(`[message] Данные TPV получены с сервера: ${event.data}`);
		let data;
		try{
			data = JSON.parse(event.data);
		}
		catch(error){
			console.log('alarmWebSocket: Parsing inbound data',error.message);
			return;
		}
		//console.log('From SignalK',data,vesselSelf);
		if(!data.updates){	// ответ на какой-то запрос
			//console.log('Other From SignalK',data);
			if(data.self){	// handshaiking
			}
			return;
		}
		byUpdates: for(let update of data.updates){	// это обновление от одного источника. Обновлений будет столько, сколько источников.
			//console.log('[alarmWebSocket.onmessage] update:',update);
			if(!update.values) continue;	// непонятно, почему может не быть values, но иногда их нет. Потому что пришло по обновлению meta
			for(let value of update.values){	// какое-то обновление данных пришло.
				//console.log('recieved data',data);
				switch(value.path){
				case 'notifications.mob':
					// Т.е., это notifications.mob, изменённый источником выше? Чёта извращённая логика...
					// Но notifications.mob глобален, так что пофигу на источник?
					// Т.е., приходит к нам весь объект MOB со всеми точками, из которого
					// мы делаем сперва mobMarkerJSON как FeatureCollection точек, 
					// а потом mobMarker как LayerGroup из FeatureCollection с Marker для каждой точки.
					// pre MOB -- даже если у нас нет координат, полезно показать маркеры MOB
					if((value.value === null) || (value.value.state == 'normal')) { 	// режим MOB надо выключить
						if(map.hasLayer(mobMarker)){ 	// если показывается мультислой с маркерами MOB
							MOBclose(); 	// пришло, что режима MOB нет -- завершим его
						}
						continue byUpdates;
					}
					//console.log('режим MOB есть, пришли новые данные',value);
					if((value.value.source == instanceSelf) && (mobMarker.feature.properties.timestamp >= Math.round(Date.parse(update.timestamp)/1000))) {	// это нами же установленный MOB, не изменённый никем.
						//console.log('это нами же установленный MOB, timestamp, пришло',mobMarker.feature.properties.timestamp,Math.round(Date.parse(update.timestamp)/1000));
						continue byUpdates;
					};
					// Почему-то при .onmessage timestamp имеется только один на всё обновление,
					// когда как через /signalk/v1/api/vessels/self - timestamp свой на каждый path.
					// Непонятно, к чему относится .onmessage timestamp, но другого у нас нет.
					let mobMarkerJSON = MOBtoGeoJSON(value.value);
					//console.log('[alarmWebSocket.onmessage] сделали из новых данных mobMarkerJSON:',mobMarkerJSON);
					if(!mobMarkerJSON) continue byUpdates;
					
					mobMarker.remove(); 	// убрать мультислой-маркер с карты
					createMOBpointMarker(mobMarkerJSON);	// Восстановим мультислой маркеров из GeoJSON
					for(const feature of mobMarkerJSON.features){
						if(feature.geometry.type != 'Point') continue;
						//console.log('[alarmWebSocket.onmessage] mobMarker feature:',feature);
						// Если какой-то маркер устанолен нами - включим запись трека
						if(is_currentMOBmarkerSelf(feature) && (loggingIndicator !== undefined && !loggingSwitch.checked)){
							loggingSwitch.checked = true;
							loggingRun(); 	// хотя в loggingSwitch стоит onChange="loggingRun();" изменение loggingSwitch.checked = true; не приводит к срабатыванию обработчика
							break;
						};
					};
					//console.log('[alarmWebSocket.onmessage] mobMarker:',mobMarker);
					MOBtabHighLight(true);	// Подсветим кнопку
					break;
				case 'notifications.danger.collision':
					//console.log('recieved collision data',value.value);
					//console.log('collision data',value.value ? value.value.vessels.length : 'null');
					//console.log('collision data',value.value ? value.value.collisionSegments : 'null');
					if(!DisplayAISswitch.checked) break;	// если в параметрах не указано показывать AIS
					collisisonDetected.clearLayers();	// очистим слой 
					collisionDirectionsCursor.clearLayers();
					if((value.value === null)||(!DisplayAISswitch.checked)){	// переключатель "Показывать AIS" выключен
						collisisonDetected.remove();	// удалить -- откуда? Мы-то знаем, что с map...
						break;	// switch
					}
					for(const vesselID in value.value.vessels){
						//console.log('Collision',value.value.vessels[vesselID]);
						// Значки опасностей
						collisisonDetected.addLayer( L.marker(value.value.vessels[vesselID], {
							icon: collisionIcon,
							opacity: 0.5,
							pane: 'overlayPane',
							zIndexOffset: -1000
						}));
						// Указатели вокруг курсора
						const selflatLng = cursor.getLatLng();
						if(selflatLng){
							//console.log('Опасность с',bearing(selflatLng, value.value.vessels[vesselID]));
							collisionDirectionsCursor.addLayer( L.marker(selflatLng, {
								icon: collisionDirectionIcon,
								opacity: 0.75,
								rotationAngle: bearing(selflatLng, value.value.vessels[vesselID])
							}));
						}
					};
					/*////////////////////////// collisionDetector test ///////////////////////////////
					for(let vesselID in value.value.collisionSegments){
						//console.log(value.value.collisionSegments[vesselID]);
						
						// Общий объемлющий прямоугольник
						if(value.value.collisionSegments[vesselID].unitedSquareAreas){
							value.value.collisionSegments[vesselID].unitedSquareAreas.forEach(area => {
								//console.log('unitedSquareArea:',area);
								let polyline = [
									[area.topLeft.latitude,area.topLeft.longitude],
									[area.bottomRight.latitude,area.topLeft.longitude],
									[area.bottomRight.latitude,area.bottomRight.longitude],
									[area.topLeft.latitude,area.bottomRight.longitude],
									[area.topLeft.latitude,area.topLeft.longitude]
								];
								collisisonDetected.addLayer(L.polyline(polyline,{color: 'green',weight: 2,}));
							});
						}
						
						// Пересекающиеся отрезки
						if(value.value.collisionSegments[vesselID].segments){
							value.value.collisionSegments[vesselID].segments.forEach(segment => {
								//console.log('collisionSegment:\n',segment);
								let polyline = [
									[segment[0][0].latitude,segment[0][0].longitude],
									[segment[0][1].latitude,segment[0][1].longitude]
								];
								collisisonDetected.addLayer(L.polyline(polyline,{color: 'yellow',weight: 6,}));
								polyline = [
									[segment[1][0].latitude,segment[1][0].longitude],
									[segment[1][1].latitude,segment[1][1].longitude]
								];
								collisisonDetected.addLayer(L.polyline(polyline,{color: 'yellow',weight: 6,}));
							});
						}
					};
					/*////////////////////////// end collisionDetector test ///////////////////////////////
					collisisonDetected.addTo(map);	// а collisionDirectionsCursor часть positionCursor, и оно и так addTo(map)
					break;
				};
			};
		};
	}; // end alarmWebSocket.onmessage

	alarmWebSocket.onclose = function(event) {
		console.log(`alarmWebSocket closed: connection broken with code ${event.code} by reason ${event.reason}`);
		window.setTimeout(alarmWebSocketStart, 3000); 	// перезапустим сокет через  секунд. В каком контексте здесь вызывается callback -- мне осталось непонятным, поэтому сокет ваще глобален
		collisisonDetected.clearLayers();	// очистим слой 
		collisionDirectionsCursor.clearLayers();
		collisisonDetected.remove();
		collisionDirectionsCursor.remove();
	}; // end alarmWebSocket.onclose

	alarmWebSocket.onerror = function(error) {
	  console.log(`[alarmWebSocket error] ${error.message}`);
	}; // end alarmWebSocket.onerror
}; // end function alarmWebSocketStart

alarmWebSocketStart(); 	// запускам периодическую функцию получать оповещения

function alarmWebSocketStop(message=''){
	console.log('Stop recieve TPV',);
	alarmWebSocket.close(1000,message);
} // end function alarmWebSocketStop


// 	Запуск периодических функций	 realtime -- в galadrielmap.js, функция, асинхронно обращающаяся к uri

// Динамическое обновление показываемых маршрутов
var updateRoutesInterval;
if(updateRouteServerURI) updateRoutesInterval = setInterval(realtime,3000,updateRouteServerURI,routeUpdate);

function routeUpdate(changedRouteNames) {
/* Вызывается из-под realtime */
//console.log('changedRouteNames:',changedRouteNames);
if(routeDisplayed.innerHTML.trim() == "") { 	// не показывается ни одного маршрута
	updateRoutesInterval = clearInterval(updateRoutesInterval); 	// прекратим следить за изменениями
	return;
}
/* в связи с возможностью наличия в trackDisplayed дублирующихся id --
может быть, вместо document.getElementById(name) сделать цикл по потомкам routeDisplayed? */
let node;
if(changedRouteNames.error) return;
for(const name of changedRouteNames){
	node = document.getElementById(name); 	// однако, в trackDisplayed могут быть те же имена. Забить? в querySelector требуется экранирование пробелов и спец-символов. Это секс.
	//console.log('[routeUpdate] node:',name,node);
	if(node.parentNode != routeDisplayed) continue; 	// элемент, конечно, всегда есть, нужно, чтобы он показывался
	//console.log('[routeUpdate] replase node:',node);
	savedLayers[name].remove(); 	// удалим слой с карты
	savedLayers[name] = null; 	// обозначим, что слоя с таким именем у нас нет
	displayRoute(node); 	// перересуем маршрут
}
} // end  function routeUpdate

// Текущий трек
// Должен обновляться, даже если обновлялка не описана в конфиге, потому что трек может писать кто-то ещё. 
// Но как его обновлять, если нет обновлялки? Если только перегружать полностью.
// Но это очень затратно. Поэтому -- нет, не должен.
// Т.е. в худшем случае -- мы не знаем, обновляется ли currentTrack, или нет
// Ещё обновление трека можно повесить на обновление координат. Это концептуально правильно, но
// тогда при потере сервиса координат пропадёт и обновление трека (потому что функция обновления
// координат перестанет вызываться). А совсем везде независимое обновление трека будет работать, и
// покажет положение даже при отсутствии сервиса координат.

// Установим переключатель в сохранённое состояние
//loggingSwitch.checked = Boolean(+getCookie('GaladrielloggingSwitch')); 	// getCookie from galadrielmap.js
loggingSwitch.checked = Boolean(storageHandler.restore('loggingSwitch')); 	// storageHandler from galadrielmap.js

var currentTrackShowedFlag = false; 	// флаг, не показывается ли текущий путь. Если об этом спрашивать у Leaflet, то пока загружается трек, можно запустить его загрузку ещё раз пять.
var currentWaitTrackUpdateProcess;	// процесс ослеживания наличия текущего (пишущегося) трека
var currentTrackUpdateProcess;	// процесс обновления текущего трека

// Это надо запускать вне зависимости от, скажем, наличия связи с сервисом координат
// потому что оно обращается к отдельным сервисам, которые могут и должны жить своей жизнью. 
if(currTrackSwitch.checked){	// Текущий трек всегда показывается
	if(currentTrackName) startCurrentTrackUpdateProcess();	// есть текущий трек
	else startCurrentWaitTrackUpdateProcess();
}

function startCurrentTrackUpdateProcess(){
// указано, откуда взять обновления
if(!currentTrackServerURI) return;
if(currentTrackUpdateProcess) return;	// оно уже запущено
// Здесь не нужно останавливать слежение за наличием пишушегося трека,
// потому что включение / выключение на сервере происходит асинхронно,
// и за появлением результата надо следить
//if(currentWaitTrackUpdateProcess) {
//	clearInterval(currentWaitTrackUpdateProcess);	// остановим слежение за наличием пишущегося трека 	
//	currentWaitTrackUpdateProcess = null;
//	console.log('[startCurrentTrackUpdateProcess]  Logging check stopped');
//}
currentTrackUpdateProcess = setInterval(currentTrackUpdate,3000);	// раз в 3 секунды слежение за треком: получение обновления и показ
console.log('[startCurrentTrackUpdateProcess] Update track started with',currentTrackName,'track');
}; // end function startCurrentTrackUpdateProcess

function currentTrackUpdate(){
/*
Global: map, savedLayers, currentTrackName, currentTrackShowedFlag
DOM objects: loggingSwitch, trackDisplayed
*/
//console.log('[currentTrackUpdate] currentTrackName='+currentTrackName,'currentTrackShowedFlag=',currentTrackShowedFlag);
if(currentTrackShowedFlag !== false) { 	 //console.log("Текущий трек некогда был загружен или сейчас загружается");
	if(map.hasLayer(savedLayers[currentTrackName])) { 	// если он реально есть
		//console.log('[currentTrackUpdate] Текущий трек есть на карте','currentTrackName='+currentTrackName,'currentTrackShowedFlag=',currentTrackShowedFlag);
		updateCurrTrack(); 	//  - обновим,  galadrielmap.js
		currentTrackShowedFlag = true;
	}
	else if(currentTrackShowedFlag == 'error'){
		//console.log('[currentTrackUpdate] Не удалось загрузить файл текущего пути');
		deSelectTrack(document.getElementById(currentTrackName),trackList,trackDisplayed,displayTrack);
	}
	else { 
		if(currentTrackShowedFlag != 'loading') currentTrackShowedFlag = false;
	}
}
else { 	 //console.log("[currentTrackUpdate] текущий трек ещё не был загружен", currentTrackName);
	//console.log('[currentTrackUpdate] document.getElementById(currentTrackName):',document.getElementById(currentTrackName));
	currentTrackShowedFlag = 'loading'; 	// укажем, что трек сейчас загружается
	// Будем показывать путь, если указано "Текущий путь всегда показывается".
	//console.log('[currentTrackUpdate] Запускаем показ текущего пути');
	selectTrack(document.getElementById(currentTrackName),trackList,trackDisplayed,displayTrack); 	// загрузим трек асинхронно. galadrielmap.js
}
//console.log('[currentTrackUpdate] Обновлён трек','currentTrackName='+currentTrackName,'currentTrackShowedFlag=',currentTrackShowedFlag);
} // end function currentTrackUpdate


function startCurrentWaitTrackUpdateProcess(){
// указано, откуда взять обновления, иначе бессмысленно следить за фактом записи трека
if(!currentTrackServerURI) return;
//if(!currTrackSwitch.checked) return;	// Текущий трек всегда показывается Думаю, здесь очень неявно применение этого условия
if(currentWaitTrackUpdateProcess) return;	// оно уже запущено
if(currentTrackUpdateProcess) {
	console.log('[startCurrentWaitTrackUpdateProcess]  Track update stopped because no logging now');
	clearInterval(currentTrackUpdateProcess);	
	currentTrackUpdateProcess = null;
}
currentWaitTrackUpdateProcess = setInterval(loggingCheck,5000);	// раз в  секунд слежение за наличием текущего трека
console.log('[startCurrentWaitTrackUpdateProcess] Logging check started',currentWaitTrackUpdateProcess);
}; // end function startCurrentWaitTrackUpdateProcess


// Сохранение переменных
var savePositionProcess = setInterval(doSavePosition,savePositionEvery); 	// велим сохранять всё каждые savePositionEvery
// Всегда после загрузки страницы "Следовать за курсором" включено
document.getElementById("followSwitch").checked = true; 	// выставим переключатель на панели Настроек в правильное положение
</script>
</html>