UNPKG

galadrielmap_sk

Version:

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

1,191 lines (1,083 loc) 123 kB
/* global depthInData, drivedPolyLineOptions, tooggleEditRoute updClaster(pointsLayer); // galadrielmap.js createSuperclaster(geojson); // galadrielmap.js depends polycolorRenderer, supercluster, Leaflet.TextPath */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.omnivore = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ "use strict" var xhr = require('corslite'), csv2geojson = require('csv2geojson'), wellknown = require('wellknown'), polyline = require('polyline'), topojson = require('topojson'), toGeoJSON = require('togeojson'); module.exports.polyline = polylineLoad; module.exports.polyline.parse = polylineParse; module.exports.geojson = geojsonLoad; module.exports.topojson = topojsonLoad; module.exports.topojson.parse = topojsonParse; module.exports.csv = csvLoad; module.exports.csv.parse = csvParse; module.exports.gpx = gpxLoad; module.exports.gpx.parse = gpxParse; module.exports.kml = kmlLoad; module.exports.kml.parse = kmlParse; module.exports.wkt = wktLoad; module.exports.wkt.parse = wktParse; //module.exports.toGeoJSON = toGeoJSON; function addData(l, d) { // layer geojson /* Загружает geojson в layer l - layer d - data */ if ('setGeoJSON' in l) { l.setGeoJSON(d); } else if ('addData' in l) { l.addData(d); } } // end function addData /** * Load a [GeoJSON](http://geojson.org/) document into a layer and return the layer. * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function geojsonLoad(url, options, customLayer) { var layer = customLayer || L.geoJson(); xhr(url, function(err, response) { if (err) return layer.fire('error', { error: err }); addData(layer, JSON.parse(response.responseText)); layer.fire('ready'); }); return layer; } /** * Load a [TopoJSON](https://github.com/mbostock/topojson) document into a layer and return the layer. * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function topojsonLoad(url, options, customLayer) { var layer = customLayer || L.geoJson(); xhr(url, onload); function onload(err, response) { if (err) return layer.fire('error', { error: err }); topojsonParse(response.responseText, options, layer); layer.fire('ready'); } return layer; } /** * Load a CSV document into a layer and return the layer. * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function csvLoad(url, options, customLayer) { if(customLayer) var layer = L.layerGroup([customLayer]); else var layer = L.layerGroup(); xhr(url, onload); function onload(err, response) { var error; if (err) return layer.fire('error', { error: err }); function avoidReady() { error = true; } layer.on('error', avoidReady); csvParse(response.responseText, options, layer); layer.off('error', avoidReady); if (!error) layer.fire('ready'); } return layer; } /** * Load a GPX document into a layer and return the layer. * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function gpxLoad(url, options, customLayer) { if(customLayer) var layer = L.layerGroup([customLayer]); else var layer = L.layerGroup(); //console.log(url); xhr(url, onload); function onload(err, response) { var error; if (err) return layer.fire('error', { error: err }); function avoidReady() { error = true; } layer.on('error', avoidReady); //console.log('leaflet-omnivore [gpxLoad] onload response:',response.responseText); gpxParse(response.responseXML || response.responseText, options, layer); // gpxParse(response.responseText, options, layer); layer.off('error', avoidReady); if (!error) layer.fire('ready'); } return layer; } /** * Load a [KML](https://developers.google.com/kml/documentation/) document into a layer and return the layer. * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function kmlLoad(url, options, customLayer) { if(customLayer) var layer = L.layerGroup([customLayer]); else var layer = L.layerGroup(); xhr(url, onload); function onload(err, response) { var error; if (err) return layer.fire('error', { error: err }); function avoidReady() { error = true; } layer.on('error', avoidReady); kmlParse(response.responseXML || response.responseText, options, layer); layer.off('error', avoidReady); if (!error) layer.fire('ready'); } return layer; } /** * Load a WKT (Well Known Text) string into a layer and return the layer * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function wktLoad(url, options, customLayer) { var layer = customLayer || L.geoJson(); xhr(url, onload); function onload(err, response) { if (err) return layer.fire('error', { error: err }); wktParse(response.responseText, options, layer); layer.fire('ready'); } return layer; } /** * Load a polyline string into a layer and return the layer * * @param {string} url * @param {object} options * @param {object} customLayer * @returns {object} */ function polylineLoad(url, options, customLayer) { var layer = customLayer || L.geoJson(); xhr(url, onload); function onload(err, response) { if (err) return layer.fire('error', { error: err }); polylineParse(response.responseText, options, layer); layer.fire('ready'); } return layer; } function topojsonParse(data, options, layer) { var o = typeof data === 'string' ? JSON.parse(data) : data; layer = layer || L.geoJson(); for (var i in o.objects) { var ft = topojson.feature(o, o.objects[i]); if (ft.features) addData(layer, ft.features); else addData(layer, ft); } return layer; } function csvParse(csv, options, layer) { /**/ //console.log('[csvParse]',csv,options,layer); if(layer) { if("getLayers" in layer) { // это layerGroup var featuresLayer = layer.getLayers()[0] || L.geoJson(); } else { // это одиночный Layer var featuresLayer = layer; layer = new L.layerGroup([featuresLayer]); // попробуем сменть тип на layerGroup, но это обычно боком выходит } } else { var featuresLayer = L.geoJson(); var layer = new L.layerGroup([featuresLayer]); } var color = 0xFFFFFF; if(typeof globalCurrentColor !== 'undefined') { // если оно определено, то определена и функция nextColor color = globalCurrentColor; globalCurrentColor = nextColor(globalCurrentColor); // сменим текущий цвет, from galadrielmap.js } if(color == 0xFFFFFF) featuresLayer.options.color = 0x3388FF; // умолчальный цвет линий else featuresLayer.options.color = color; // цвет линий if(options.featureNameNode) { // li с именем файла, из которого делаем layer options.featureNameNode.style.backgroundColor = '#'+('000000' + color.toString(16)).slice(-6); } featuresLayer.options.onEachFeature = getPopUpToLine; // функция, вызываемая для каждой feature при её создании featuresLayer.options.style = function(geoJsonFeature){return{color: '#'+('000000' + featuresLayer.options.color.toString(16)).slice(-6)};}; // A Function defining the Path options for styling GeoJSON lines and polygons, called internally when data is added. if(! layer.hasLayer(featuresLayer)) layer.addLayer(featuresLayer); var pointsLayer = L.geoJson(); pointsLayer.options.color = color; // цвет значков pointsLayer.options.pointToLayer = function (geoJsonPoint, latlng) { // функция, вызываемая для каждой точки при её создании let marker; if(geoJsonPoint.properties.marker) { //console.log('Маркер уже есть'); marker = geoJsonPoint.properties.marker.setLatLng(latlng); } else { // parameters тут -- это options создаваемого маркера, плюс разное, типа - color //console.log('Новый маркер'); let parameters = {color: pointsLayer.options.color}; // таким образом мы забросим цвет в создание маркера marker = getMarkerToPoint(geoJsonPoint, latlng, parameters); geoJsonPoint.properties.marker = marker; }; return marker; }; layer.addLayer(pointsLayer); options = options || {}; csv2geojson.csv2geojson(csv, options, onparse); function onparse(err, geojson) { if (err) return layer.fire('error', { error: err }); //console.log('[onparse] geojson:',geojson); var Points=[]; var Features=[]; //console.log(layer.options.markerColor); for(var i=0; i<geojson.features.length;i++) { if(geojson.features[i].geometry.type=='Point') { geojson.features[i].properties.color = layer.options.markerColor; Points.push(geojson.features[i]); } else Features.push(geojson.features[i]); } addData(featuresLayer, Features); // добавим и покажем всё остальное if(Points.length) { doClastering(pointsLayer, Points); // закластеризуем точки updClaster(pointsLayer); // galadrielmap.js и покажем } } // end function onparse return layer; } // end function csvParse function gpxParse(gpx, options, layer) { /* Создаёт layerGroup из двух слоёв, в одном - линии, в другом - точки. */ //console.log('leaflet-omnivore [gpxParse] gpx:',gpx); if(layer) { //console.log('leaflet-omnivore [gpxParse] layer:',layer,layer instanceof L.layerGroup,"getLayers" in layer); if(layer instanceof L.LayerGroup) { // это layerGroup //if("getLayers" in layer) { // это layerGroup var featuresLayer = layer.getLayers()[0] || L.geoJson(); } else { // это одиночный Layer var featuresLayer = layer; layer = L.layerGroup([featuresLayer]); // попробуем сменть тип на layerGroup, но это обычно боком выходит. Но, вообще-то, layer создаётся как layerGroup. } } else { var featuresLayer = L.geoJson(); layer = L.layerGroup([featuresLayer]); }; var xml = parseXML(gpx); // делает DOM XML, если gpx -- строка, иначе не делает ничего const errorNode = xml.querySelector("parsererror"); if (errorNode) return layer.fire('error', { error: 'Could not parse GPX' }); //console.log('leaflet-omnivore [gpxParse] xml:',xml); var geojson = toGeoJSON.gpx(xml); //console.log('leaflet-omnivore [gpxParse] geojson:',geojson); if(layer.properties) Object.assign(layer.properties,geojson.properties); else layer.properties = geojson.properties; var Points=[]; var Features=[]; for(let i=0; i<geojson.features.length;i++) { if(geojson.features[i].geometry.type=='Point') Points.push(geojson.features[i]); else { //console.log(geojson.features[i]); //if(geojson.features[i].properties.isRoute) geojson.features[i].properties.fileName = options.featureNameNode.innerText.trim(); // оно не надо Features.push(geojson.features[i]); } } //console.log('leaflet-omnivore [gpxParse] options:',options); //console.log('leaflet-omnivore [gpxParse] Points:',Points); //console.log('leaflet-omnivore [gpxParse] Features:',Features); var color = 0xFFFFFF; if(typeof globalCurrentColor !== 'undefined') { // если оно определено, то определена и функция nextColor color = globalCurrentColor; globalCurrentColor = nextColor(globalCurrentColor); // сменим текущий цвет, from galadrielmap.js } if(color == 0xFFFFFF) featuresLayer.options.color = 0x3388FF; // умолчальный цвет линий else featuresLayer.options.color = color; // цвет линий if(options && options.featureNameNode) { // li с именем файла, из которого делаем layer options.featureNameNode.style.backgroundColor = '#'+('000000' + color.toString(16)).slice(-6); } featuresLayer.options.onEachFeature = function (feature, layer){ // функция, вызываемая для каждой feature при её создании getPopUpToLine(feature, layer); //console.log('[featuresLayer.options.onEachFeature] feature:',feature); //console.log('[featuresLayer.options.onEachFeature] layer:',layer); //console.log('[featuresLayer.options.onEachFeature] depthInData:',depthInData,options.featureNameNode.classList.contains('currentTrackName')); // Leaflet.TextPath несовместимо с polycolorRenderer, разбираться лень, поэтому стрелочки // будем лепить только если нет данных о глубине или не велено глубину показывать // лепить стрелочки на линию, только если это не текущий трек, который всё время перерисовывается. Ибо чёта стрелочки затратно... if(!(depthInData && depthInData.display && feature.properties && feature.properties.depths) && !(options && options.featureNameNode && options.featureNameNode.classList && options.featureNameNode.classList.contains('currentTrackName')) ){ //console.log('Рисуем стрелочки направления движения'); layer.setText(' > ', {repeat: true, offset: '0.6ch', // сдвиг вправо от линии на половину ширины символа (плюс поправочка) размером font-size. ch - Предварительная мера (ширина) глифа "0" шрифта элемента attributes: {fill: layer.options.color, 'font-size': '1.5rem', 'font-weight': 'bold', 'opacity': 0.7 } }); // Leaflet.TextPath }; }; featuresLayer.options.style = function(geoJsonFeature){ // A Function defining the Path options for styling GeoJSON lines and polygons, called internally when data is added. // вот тут надо вычислить цвета и указать рендерер let style = {}; //console.log('leaflet-omnivore.js [featuresLayer.options.style] geoJsonFeature:',geoJsonFeature); //console.log('leaflet-omnivore.js [featuresLayer.options.style] depthInData:',depthInData); if(typeof depthInData !== 'undefined' && depthInData.display && geoJsonFeature.properties && geoJsonFeature.properties.depths){ // depthInData - global from options.js let colors = [], weights = []; for(let i=0; i < geoJsonFeature.properties.depths.length; i++){ if(Array.isArray(geoJsonFeature.properties.depths[i])) { if(!geoJsonFeature.geometry.coordinates[i].length) continue; // если этот сегмент GeoJSON MultiLineString пустой (например, сделан из <trkseg></trkseg>), то Leaflet этот сегмент вообще опустит, что ему не говори. let colors1 = [], weights1 = []; for(let depth of geoJsonFeature.properties.depths[i]){ if(depth === null){ colors1.push(null); weights1.push(null); } else { colors1.push(value2color(depth,depthInData.minvalue||0,depthInData.maxvalue||10,depthInData.minColor||[255,0,0],depthInData.maxColor||[0,255,0],depthInData.underMinColor||"rgb(155,0,0)",depthInData.upperMaxColor||"rgb(200,250,240)")); weights1.push(5); } } const _weights = [...new Set(weights1)]; if(_weights.length == 1) weights1 = _weights; colors.push(colors1); weights.push(weights1); } else { if(geoJsonFeature.properties.depths[i] === null){ colors.push(null); weights.push(null); } else { //console.log('leaflet-omnivore.js [featuresLayer.options.style] depth=',geoJsonFeature.properties.depths[i],'value2color:',value2color(geoJsonFeature.properties.depths[i],depthInData.minvalue,depthInData.maxvalue)); colors.push(value2color(geoJsonFeature.properties.depths[i],depthInData.minvalue||0,depthInData.maxvalue||10,depthInData.minColor||[255,0,0],depthInData.maxColor||[0,255,0],depthInData.underMinColor||"rgb(155,0,0)",depthInData.upperMaxColor||"rgb(200,250,240)")); weights.push(5); } } } const _weights = [...new Set(weights)]; if(_weights.length == 1) weights = _weights; //console.log('leaflet-omnivore.js [featuresLayer.options.style] colors:',colors,'weights:',weights); style = { noClip: true, // отключить всякое упрощение линии Leaflet'ом smoothFactor: 0, renderer: new polycolorRenderer(), color: '#'+('000000' + featuresLayer.options.color.toString(16)).slice(-6), colors: colors, useGradient: true, opacity: 0.8, weights: weights } } else { style.color = '#'+('000000' + featuresLayer.options.color.toString(16)).slice(-6); style.opacity = 0.7; // если применять на суше, то непрозрачная линия ложится на тропу, и непонятно. Слегка прозрачная решает проблему. //console.log('style.color:',style.color); // поскольку параметры из drivedPolyLineOptions применяются позже... А почему они позже? if(geoJsonFeature.properties && geoJsonFeature.properties.isRoute){ style.dashArray = `0,${drivedPolyLineOptions.options.weight+2}`; // [длина, толщина] Если длина 0, то будут кружочки. где я в этом месте достану ширину рисуемой линии, если она ещё не задана? А drivedPolyLineOptions для этого и предназначено. //style.lineCap = "bitt"; } else { }; } return style; }; // end featuresLayer.options.style = function // Добавим в слой объекты featuresLayer.addData(Features); // добавим и покажем всё остальное if(! layer.hasLayer(featuresLayer)) layer.addLayer(featuresLayer); //console.log(featuresLayer); // Теперь добавим точки if(Points.length) { var pointsLayer = L.geoJson(); pointsLayer.options.color = color; // цвет значков // pointToLayer вызывается рашьше, чем onEachFeature, и всё это вызывается в addData pointsLayer.options.pointToLayer = function (geoJsonPoint, latlng) { // функция, вызываемая для каждой точки при её создании //console.log('[csvParse] pointToLayer',geoJsonPoint); let marker; if(geoJsonPoint.properties.marker) { //console.log('Маркер уже есть'); marker = geoJsonPoint.properties.marker.setLatLng(latlng); } else { // parameters тут -- это options создаваемого маркера, плюс разное, типа - color //console.log('Новый маркер'); let parameters = {color: pointsLayer.options.color}; // таким образом мы забросим цвет в создание маркера marker = getMarkerToPoint(geoJsonPoint, latlng, parameters); if(typeof tooggleEditRoute === 'function') { //marker.on('dblclick', L.DomEvent.stop).on('dblclick', tooggleEditRoute); //marker.on('click', L.DomEvent.stop).on('click', tooggleEditRoute); // galadrielmap.js чёта stop не работает? marker.on('click', tooggleEditRoute); // galadrielmap.js } marker.on('editable:dragstart', function(event){ // Нужно будет перестроить superclaster с точкой с новыми координатами removeFromSuperclaster(pointsLayer,event.target); // galadrielmap.js }); marker.on('editable:dragend', function(event){ // Нужно перестроить superclaster с точкой с новыми координатами //console.log('leaflet-omnivore.js [marker.on editable:dragend] pointsLayer:',pointsLayer); pointsLayer.supercluster.points.push(event.target.toGeoJSON()); pointsLayer.supercluster = createSuperclaster(pointsLayer.supercluster.points); // galadrielmap.js создание нового и загрузка в суперкластер точек }); //geoJsonPoint.properties.marker = marker; } return marker; }; doClastering(pointsLayer, Points); // закластеризуем точки updClaster(pointsLayer); // galadrielmap.js и покажем layer.addLayer(pointsLayer); } //layer.options.fileName = options.featureNameNode.innerText.trim(); // Оно не надо? //console.log(layer); //console.log(layer.getLayers()); return layer; }; // end function gpxParse function doClastering(layer, geojson) { /* Кластеризует wpt в layer, если они там есть Требует наличия supercluster.js */ /* const index = new Supercluster({ log: false, // вывод лога в консоль radius: 40, extent: 256, maxZoom: 15, }).load(geojson); // собственно, загрузка в суперкластер точек index */ layer.supercluster = createSuperclaster(geojson); // galadrielmap.js layer.on('click', (e) => { // клик по любому значку (вообще по любому месту?) :-( потому что нам нужен layer //console.log('leaflet-omnivore.js : doClastering start by click'); //console.log(e); if (e.layer.feature.properties.cluster_id) { // кликнутый значёк - кластер const expansionZoom = e.target.supercluster.getClusterExpansionZoom(e.layer.feature.properties.cluster_id); // получим масштаб, при котором этот кластер разделится map.flyTo(e.latlng,expansionZoom); } }); return layer; } // end function doClastering function getMarkerToPoint(geoJsonPoint, latlng, parameters) { // https://leafletjs.com/reference-1.3.4.html#geojson // Функция, которая в latlng рисует маркер по сведениям из geoJsonPoint // обычно вызывается как свойство layer.options.pointToLayer // В geoJsonPoint.properties собираются: //'ele' 'name', 'cmt', 'desc', 'src', 'number', 'author', 'copyright', 'sym', 'type', 'time', 'keywords' в function getProperties(node) для gpx // 'name' 'icon' 'description' в function getPlacemark(root) для kml // для csv просто берутся имеющиеся имена атрибутов, поэтому будем парсить имена отсюда: https://www.gpsbabel.org/htmldoc-1.5.4/fmt_unicsv.html //console.log(parameters); // Сам маркер - Marker if(!parameters) parameters = {}; if(!parameters.color) parameters.color = 0xFFFFFF; let marker = L.marker(latlng,parameters); // маркер для этой точки marker.options.riseOnHover = true; //console.log('[getMarkerToPoint] marker:',marker,'parameters:',parameters); if(geoJsonPoint.properties.cluster) { // это кластер //console.log(geoJsonPoint); const icon = L.divIcon({ html: `<div style="background-color: #${('000000' + parameters.color.toString(16)).slice(-6)};"><span>${ geoJsonPoint.properties.point_count_abbreviated }</span></div>`, className: `marker-cluster`, iconSize: L.point(25, 25), }); marker.setIcon(icon); } else { // это индивидуальная точка //console.log('marker for point'); // Значёк - Icon //alert('icon' in marker.options); let iconNames = []; // возможные имена значков if(geoJsonPoint.properties.sym) iconNames.push(geoJsonPoint.properties.sym.trim().replace(/ /g, '_').replace(/,/g, '').toLowerCase()); // gpx sym (symbol name) attribyte if(geoJsonPoint.properties.symbol) iconNames.push(geoJsonPoint.properties.symbol.trim().replace(/ /g, '_').replace(/,/g, '').toLowerCase()); // csv symbol name attribyte if(geoJsonPoint.properties.symb) iconNames.push(geoJsonPoint.properties.symb.trim().replace(/ /g, '_').replace(/,/g, '').toLowerCase()); // csv symbol name attribyte if(geoJsonPoint.properties.type) iconNames.push(geoJsonPoint.properties.type.trim().replace(/ /g, '_').replace(/,/g, '').toLowerCase()); // gpx type (classification) attribyte if(geoJsonPoint.properties.icon) { // kml Icon //console.log('"'+geoJsonPoint.properties.icon.textContent.trim()+'"'); let iNm = geoJsonPoint.properties.icon.textContent.trim(); iNm = iNm.substring(iNm.lastIndexOf('/')+1); let iNmExt = iNm.slice((iNm.lastIndexOf(".") - 1 >>> 0) + 2); // icon filename ext https://www.jstips.co/en/javascript/get-file-extension/ потому что там нет естественного пути if(iNmExt.length) iNm = iNm.slice(0,-(iNmExt.length+1)); //console.log(iNm); if(iNm.length) iconNames.push(iNm.replace(/ /g, '_').replace(/,/g, '').toLowerCase()); // kml icon name in <Style><IconStyle><Icon> attribyte } iconNames = [...new Set(iconNames)]; // только уникальные значения. Сначала из неуникального массива делается Set, потом из Set -- массив. //console.log("[getMarkerToPoint] iconNames:",iconNames); iconServer.setIconCustomIcon(marker,iconNames); // заменить в marker icon на нужный асинхронно //console.log(iconServer.iconsByType); //console.log('[getMarkerToPoint] marker:',marker); // Подпись - Tooltip if(geoJsonPoint.properties.name) { marker.bindTooltip(geoJsonPoint.properties.name,{ permanent: true, // всегда показывать //direction: 'auto', //direction: 'left', direction: 'top', offset: [-16,0], className: 'wpTooltip', // css class opacity: 0.65, pane: 'overlayPane', zIndexOffset: -600 }); //console.log('[getMarkerToPoint] _tooltip:',marker._tooltip); // поскольку ._container появляется только после добавления на карту, изменение цвета // выполняется по соответствующему событию. // Но вообще нормального способа динамически менять цвет нет. // Устанавливается цвет фона контейнера тултипа в полупрозрачный. Оно не надо: оно потом делается в соответствии со свойствами. marker._tooltip.on('add',function (event){ //event.target._container.style.backgroundColor = `#${('000000' + parameters.color.toString(16)).slice(-6)}80`; event.target._container.style.backgroundColor = `#${('000000' + parameters.color.toString(16)).slice(-6)}`; }); //}).openTooltip(); // и перерисуем подпись под умолчальный маркер. Под другие маркеры перерисуем потом. Но это бессмысленно - она не перерисовывается } // Информация о - PopUp //console.log(geoJsonPoint.properties.link); var popUpHTML = ''; if(geoJsonPoint.properties.number) popUpHTML = geoJsonPoint.properties.number; if(geoJsonPoint.properties.name) popUpHTML = "<b>"+geoJsonPoint.properties.name+"</b> "+popUpHTML; if(!popUpHTML) popUpHTML = latlng.lat+" "+latlng.lng; //console.log('leaflet-omnivore.js [getMarkerToPoint] latlng:',latlng); popUpHTML = "<span style='font-size:120%'; onClick='doCopyToClipboard(\""+latlng.lat+" "+latlng.lng+"\")'>" + popUpHTML + "</span><br>"; //popUpHTML = "<span style='font-size:120%';'>" + popUpHTML + "</span><br>"; if(geoJsonPoint.properties.cmt) popUpHTML = popUpHTML+"<p>"+geoJsonPoint.properties.cmt.replace(/\n/g, '<br>')+"</p>"; // gpx description; if(geoJsonPoint.properties.desc) popUpHTML = popUpHTML+"<p>"+geoJsonPoint.properties.desc.replace(/\n/g, '<br>')+"</p>"; // gpx description if(geoJsonPoint.properties.notes) popUpHTML = popUpHTML+"<p>"+geoJsonPoint.properties.notes.replace(/\n/g, '<br>')+"</p>"; // csv description if(geoJsonPoint.properties.description) popUpHTML = popUpHTML+"<p>"+geoJsonPoint.properties.description.replace(/\n/g, '<br>')+"</p>"; // kml description if(geoJsonPoint.properties.comment) popUpHTML = popUpHTML+"<p>"+geoJsonPoint.properties.comment.replace(/\n/g, '<br>')+"</p>"; // csv description if(geoJsonPoint.properties.ele) popUpHTML = popUpHTML+"<p>Alt: "+geoJsonPoint.properties.ele+"</p>"; // gpx elevation if(geoJsonPoint.properties.alt) popUpHTML = popUpHTML+"<p>Alt: "+geoJsonPoint.properties.alt+"</p>"; // csv elevation if(geoJsonPoint.properties.height) popUpHTML = popUpHTML+"<p>Alt: "+geoJsonPoint.properties.height+"</p>"; // csv elevation if(geoJsonPoint.properties.depth) popUpHTML = popUpHTML+"<p>Alt: "+geoJsonPoint.properties.depth+"</p>"; // csv depth popUpHTML += getLinksHTML(geoJsonPoint); // приклеим ссылки marker.bindPopup(popUpHTML+'<br>'); // создадим PopUp, popUpHTML всегда не пуст } return marker; } // end function getMarkerToPoint function getLinksHTML(feature) { /* Возвращает строку,которую можно было бы показать в PopUp, из атрибутов link в feature. Оформляет ссылки как может. Пытается обнаружить ссылки на картинки и показывает для них фотоаппаратик. */ var camImgPath = leafletOmnivoreScript.src.substr(0, leafletOmnivoreScript.src.lastIndexOf("/"))+"/icons/cam.svg"; var popUpHTML = ''; var links = []; if(feature.properties.link) { if(typeof(feature.properties.link)=='object'){ // должно быть, это массив, который получается при импорте сложных объектов feature.properties.link.forEach(link=>links.push(link)); } else links.push(feature.properties.link); // а тут, наверно, единичный объект } if(feature.properties.url) links.push(feature.properties.url); if(!links.length) return popUpHTML; // имеются ссылки //console.log('[getLinksHTML] имеются ссылки',links); // Это может быть как просто строка с url из csv, так и строковое представление linkType gpx let parser = new DOMParser(); for(let linkStr of links){ let linkHTML='', url='', text='', mimeType=''; let link = parser.parseFromString(linkStr, "application/xml"); //console.log('[getLinksHTML] link:',linkStr,link,'parseerror',link.querySelector("parsererror")); if(link.querySelector("parsererror")) url = linkStr; // типа, там была просто строка, хотя хрен его знает, как этот кривой парсер относится к просто строкам else { url = link.activeElement.attributes.href.value; if(text = link.querySelector("text")) text = text.innerHTML; else text = ''; if(mimeType = link.querySelector("type")) mimeType = mimeType.innerHTML; else mimeType = ''; //console.log('[getLinksHTML] link object:',url,text,mimeType); } linkHTML = '<a href="'+url+'" target="_blank" >'; if(mimeType.startsWith('image') || (url.slice(-5).toLowerCase()=='.jpeg') || (url.slice(-4).toLowerCase()=='.jpg') || (url.slice(-4).toLowerCase()=='.png') || (url.slice(-4).toLowerCase()=='.svg') || (url.slice(-4).toLowerCase()=='.tif') || (url.slice(-5).toLowerCase()=='.tiff')) { linkHTML += '<img src="'+camImgPath+'" width="12%" style="vertical-align: middle; margin:auto 1rem;"></a>'; } if(text) linkHTML += ' '+text; else linkHTML += ' External link'; linkHTML += '</a><br>'; popUpHTML += linkHTML; }; if(popUpHTML) popUpHTML = '<br>'+popUpHTML; return popUpHTML; }; // end function getLinksHTML var popupDepthInfo = L.popup(); // popup для отображения глубины на путях с глубиной function getPopUpToLine(feature, layer) { /* A Function that will be called once for each created Feature ПОэтому тут не только подпись и всплывающее окно, но и прочие параметры линии */ //console.log('leaflet-omnivore [getPopUpToLine] feature:',feature,'layer:',layer); if(feature.properties && feature.properties.isRoute) { // это маршрут. //console.log('leaflet-omnivore [getPopUpToLine] drivedPolyLineOptions:',drivedPolyLineOptions,globalCurrentColor); Object.assign(layer.options,drivedPolyLineOptions.options); // drivedPolyLineOptions из index.php Object.assign(layer.feature.properties,drivedPolyLineOptions.feature.properties); // drivedPolyLineOptions из index.php layer.on('editable:editing', function (event){event.target.updateMeasurements();}); // обновлять расстояния при редактировании if(typeof tooggleEditRoute === 'function') { //layer.on('dblclick', L.DomEvent.stop).on('dblclick', tooggleEditRoute); //layer.on('click', L.DomEvent.stop).on('click', tooggleEditRoute); // galadrielmap.js чёта stop не работает? layer.on('click', tooggleEditRoute); // galadrielmap.js } } if(feature.properties) { // Подпись - Tooltip if(feature.properties.name) { layer.bindTooltip(feature.properties.name,{ permanent: true, // всегда показывать direction: 'auto', //direction: 'center', //offset: [0,-15], className: 'wpTooltip', // css class opacity: 0.75 }); } // PopUp var popUpHTML = ''; if(feature.properties.number) popUpHTML = " <span style='font-size:120%;'>"+feature.properties.number+"</span> "+popUpHTML; if(feature.properties.name) popUpHTML = "<b>"+feature.properties.name+"</b> "+popUpHTML; if(feature.properties.cmt) popUpHTML += "<p>"+feature.properties.cmt+"</p>"; if(feature.properties.desc) popUpHTML += "<p>"+feature.properties.desc.replace(/\n/g, '<br>')+"</p>"; // gpx description if(feature.properties.description) popUpHTML += "<p>"+feature.properties.description.replace(/\n/g, '<br>')+"</p>"; // kml description popUpHTML += getLinksHTML(feature); // приклеим ссылки //if(feature.properties.name) popUpHTML = "<b>"+feature.properties.name+"</b> "+popUpHTML; if(popUpHTML) { layer.bindPopup(popUpHTML+'<br>'); } if(typeof depthInData !== 'undefined' && depthInData.display && feature.properties.depths){ // есть глубина и её надо показывать depthInData - global from options.js layer.on('click', function(event) { //console.log('leaflet-omnivore [gpxParse] event:',event); // при наличии feature.geometry.coordinates let index = getNearestSegPoint(event.latlng, feature.geometry.coordinates); if(feature.properties.depths[index] !== null){ popupDepthInfo.setLatLng({lng:feature.geometry.coordinates[index][0],lat:feature.geometry.coordinates[index][1]}); // в GeoJSON наоборот, чем в Leaflet popupDepthInfo.setContent(dashboardDepthMesTXT+' '+(Math.round(feature.properties.depths[index]*100)/100)+' '+dashboardMeterMesTXT); map.openPopup(popupDepthInfo); } }) } } function getNearestSegPoint(latlng, latlngs){ /* отыскивает координату в latlngs, ближайшую к latlng широта и долгота -- как в GeoJSON, не как в Leaflet!!!! */ let distance, minDistance=999999999999, index; let latitude = latlng.lat; let longitude = latlng.lng; for (let i = 0; i < latlngs.length; i++) { distance = equirectangularDistance([longitude,latitude],latlngs[i]); if(distance < minDistance){ minDistance = distance; index = i; } //console.log('leaflet-omnivore [getNearestSegPoint] i=',i,'distance=',distance,'minDistance=',minDistance) } return index; } // end function getNearestSegPoint //console.log('leaflet-omnivore [getPopUpToLine] layer:',layer); } // end function getPopUpToLine function equirectangularDistance(from,to){ // https://www.movable-type.co.uk/scripts/latlong.html // from,to: как в GeoJSON, не как в Leaflet!!!! let from_longitude = from[0]; let from_latitude = from[1]; let to_longitude = to[0]; let to_latitude = to[1]; const rad = Math.PI/180; const φ1 = from_latitude * rad; const φ2 = to_latitude * rad; const Δλ = (to_longitude-from_longitude) * rad; const R = 6371e3; // метров const x = Δλ * Math.cos((φ1+φ2)/2); const y = (φ2-φ1); const d = Math.sqrt(x*x + y*y) * R; // метров return d; } // end function equirectangularDistance // определение имени файла этого скрипта var scripts = document.getElementsByTagName('script'); var index = scripts.length - 1; // это так, потому что эта часть сработает при загрузке скрипта, и он в этот момент - последний http://feather.elektrum.org/book/src.html var leafletOmnivoreScript = scripts[index]; //console.log(leafletOmnivoreScript); var iconServer = { // типа, объект, централизованно раздающий L.icon, в которых уже есть сама картинка как base64 // объект скачивает требуемые файлы картинок и хранит. Когда надо -- указывает в объекте L.icon как iconUrl. // Неиспользуемые картинки не удаляются, так что при удаче можно закачать в память все 400 картинок. // Но это меньше 600Kb. // Основная цель предварительной закачки картинок -- определить наличие файла с таким именем. // Список возможных имён iconNames получен из разных мест показываемого (gpx, kml, csv) файла. Там, // в принципе, могут быть разные слова, которые могут быть поняты как тип объекта, и которые // могут стать именем файла значка для объекта. Но значка с таким именем в коллекции может не быть. // Чтобы это понять, и установить умолчальный значёк -- и используется предварительная загрузка файла. iconsByType: {}, // сюда будем складывать L.icon каждого типа setIconCustomIcon: function (marker,iconNames) { /* пытается создать L.icon с iconUrl из iconNames, где они без пути и расширения при наличии такого файла - создаёт, устанавливает эту L.icon в marker и складывает в iconsByType */ let iconName = iconNames.shift(); //console.log("[iconServer] iconName=",iconName); if(!iconName) return; //console.log(this.iconsByType); if(this.iconsByType[iconName]) { if(typeof this.iconsByType[iconName] === 'object') { // такая icon уже получена marker.setIcon(this.iconsByType[iconName]).openTooltip(); // если icon с таким именем уже создавали - посадить значёк и перерисовать подпись //console.log('icon '+iconName+' из хранилища'); } else { // такую icon кто-то сейчас получает // ждать let vait = setInterval(function(){ // запустим асинхронное ожидание. В результате сначала присвоится умолчальный значёк, а потом - нужный //console.log('Ждём icon '+iconName); if(iconServer.iconsByType[iconName] && typeof iconServer.iconsByType[iconName] === 'object') { // такая icon уже получена marker.setIcon(iconServer.iconsByType[iconName]).openTooltip(); // если icon с таким именем уже создавали посадить значёк и перерисовать подпись //console.log('Дождались icon '+iconName); clearInterval(vait); // прекратим ждать } else { if(iconServer.iconsByType[iconName] === false) { //console.log('Не дождались icon '+iconName); clearInterval(vait); // оно обломалось, прекратим ждать } } },100); // таймер на милисекунд } } else if(this.iconsByType[iconName] === false) { // такой icon вообще нет, её кто-то пытался получить, но безуспешно //console.log('Уже был облом с icon '+iconName); iconServer.setIconCustomIcon(marker,iconNames); // вызовем себя для следующего имени // ничего не делать - поставится умолчальная } else { // такая icon ещё не получена this.iconsByType[iconName] = true; // укажем, что понеслось получать // получить асинхронно // все требуемые картинки значков скачиваются и хранятся в памяти // а нафига? А так мы узнаем, какой картинки нет. //console.log(leafletOmnivoreScript.src.substr(0, leafletOmnivoreScript.src.lastIndexOf("/")),iconName); fetch(leafletOmnivoreScript.src.substr(0, leafletOmnivoreScript.src.lastIndexOf("/"))+"/symbols/"+iconName+".png") .then(function(response) { //console.log(response); if(response.ok) return response.blob(); // руками обработаем ошибки сервера else throw new Error('Network response was not ok for icon '+iconName); // Перейдём сразу к .catch }) .then(function(blob){ let iconURL = URL.createObjectURL(blob); // здесь получается blob -- такой хитрый Data URL. В результате его понимает L.icon как ссылку, но файл уже загружен. Вопрос выгрузки остаётся открытым: ведь оно нужновсё время после загрузки, и загружается только один раз. https://developer.mozilla.org/ru/docs/Web/API/URL/createObjectURL //console.log(iconURL); let icon = L.icon({ iconUrl: iconURL, iconSize: [32, 37], iconAnchor: [16, 37], tooltipAnchor: [16,-25], className: 'wpIcon' }); iconServer.iconsByType[iconName] = icon; // сохраним полученный значёк в кеше marker.setIcon(icon).openTooltip(); // посадить значёк и перерисовать подпись //console.log('Create and Set icon '+iconName); //console.log(marker); }) .catch(function(error) { // - не работает в случае 404!, поэтому выше throw new Error iconServer.iconsByType[iconName] = false; // укажем, что со значком облом console.log('iconServer setIconCustomIcon fetch error: ' + error.message); iconServer.setIconCustomIcon(marker,iconNames); // вызовем себя для следующего имени }); } }, // end function setIconCustomIcon, список атрибутов объекта продолжается } // end object iconServer function kmlParse(gpx, options, layer) { /**/ if(layer) { if("getLayers" in layer) { // это layerGroup var featuresLayer = layer.getLayers()[0] || L.geoJson(); } else { // это одиночный Layer var featuresLayer = layer; layer = new L.layerGroup([featuresLayer]); // попробуем сменть тип на layerGroup, но это обычно боком выходит } } else { var featuresLayer = L.geoJson(); var layer = new L.layerGroup([featuresLayer]); }; var xml = parseXML(gpx); // делает DOM XML, если gpx -- строка, иначе не делает ничего const errorNode = xml.querySelector("parsererror"); if (errorNode) return layer.fire('error', { error: 'Could not parse KML' }); var geojson = toGeoJSON.kml(xml); var Points=[]; var Features=[]; for(var i=0; i<geojson.features.length;i++) { if(geojson.features[i].geometry.type=='Point') Points.push(geojson.features[i]); else Features.push(geojson.features[i]); }; var color = 0xFFFFFF; if(typeof globalCurrentColor !== 'undefined') { // если оно определено, то определена и функция nextColor color = globalCurrentColor; globalCurrentColor = nextColor(globalCurrentColor); // сменим текущий цвет, from galadrielmap.js } if(color == 0xFFFFFF) featuresLayer.options.color = 0x3388FF; // умолчальный цвет линий else featuresLayer.options.color = color; // цвет линий if(options.featureNameNode) { // li с именем файла, из которого делаем layer options.featureNameNode.style.backgroundColor = '#'+('000000' + color.toString(16)).slice(-6); } featuresLayer.options.onEachFeature = function (feature, layer){ // функция, вызываемая для каждой feature при её создании //console.log('[featuresLayer.options.onEachFeature] feature',feature); //console.log('KML [featuresLayer.options.onEachFeature] layer',layer); getPopUpToLine(feature, layer); if(!options.featureNameNode.classList.contains('currentTrackName')){ // лепить стрелочки на линию, только если это не текущий трек, который всё время перерисовывается. Ибо чёта стрелочки затратно... layer.setText(' > ', {repeat: true, offset: '0.6ch', // сдвиг вправо от линии на половину ширины символа (плюс поправочка) размером font-size. ch - Предварительная мера (ширина) глифа "0" шрифта элемента attributes: {fill: layer.options.color, 'font-size': '1.5rem', 'font-weight': 'bold', 'opacity': 0.7 } }); // Leaflet.TextPath }; }; featuresLayer.options.style = function(geoJsonFeature){ // A Function defining the Path options for styling GeoJSON lines and polygons, called internally when data is added. const color = '#'+('000000' + featuresLayer.options.color.toString(16)).slice(-6); return{color: color, opacity: 0.7}; }; // end featuresLayer.options.style = function addData(featuresLayer, Features); // добавим и покажем всё остальное if(! layer.hasLayer(featuresLayer)) layer.addLayer(featuresLayer); if(Points.length) { var pointsLayer = L.geoJson(); pointsLayer.options.color = color; // цвет значков pointsLayer.options.pointToLayer = function (geoJsonPoint, latlng) { // функция, вызываемая для каждой точки при её создании let marker; if(geoJsonPoint.properties.marker) { //console.log('Маркер уже есть'); marker = geoJsonPoint.properties.marker.setLatLng(latlng); } else { // parameters тут -- это options создаваемого маркера, плюс разное, типа - color //console.log('Новый маркер'); let parameters = {color: pointsLayer.options.color}; // таким образом мы забросим цвет в создание маркера marker = getMarkerToPoint(geoJsonPoint, latlng, parameters); geoJsonPoint.properties.marker = marker; } return marker; }; doClastering(pointsLayer, Points); // закластеризуем точки updClaster(pointsLayer); // galadrielmap.js и покажем layer.addLayer(pointsLayer); } return layer; } function polylineParse(txt, options, layer) { layer = layer || L.geoJson(); options = options || {}; var coords = polyline.decode(txt, options.precision); var geojson = { type: 'LineString', coordinates: [] }; for (var i = 0; i < coords.length; i++) { // polyline returns coords in lat, lng order, so flip for geojson geojson.coordinates[i] = [coords[i][1], coords[i][0]]; } addData(layer, geojson); return layer; } function wktParse(wkt, options, layer) { layer = layer || L.geoJson(); var geojson = wellknown(wkt); addData(layer, geojson); return layer; } function parseXML(str) { //console.log("[parseXML] str:",str); if (typeof str === 'string') { return (new DOMParser()).parseFromString(str, 'application/xml'); } else { return str; } } },{"corslite":3,"csv2geojson":4,"polyline":6,"togeojson":9,"topojson":10,"wellknown":11}],2:[function(require,module,exports){ },{}],3:[function(require,module,exports){ function corslite(url, callback, cors) { var sent = false; if (typeof window.XMLHttpRequest === 'undefined') { return callback(Error('Browser not supported')); } if (typeof cors === 'undefined') { var m = url.match(/^\s*https?:\/\/[^\/]*/); cors = m && (m[0] !== location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '')); } var x = new window.XMLHttpRequest(); function isSuccessful(status) { return status >= 200 && status < 300 || status === 304; } /* if (cors && !('withCredentials' in x)) { // IE8-9 x = new window.XDomainRequest(); // Ensure callback is never called synchronously, i.e., before // x.send() returns (this has been observed in the wild). // See https://github.com/mapbox/mapbox.js/issues/472 // Это костыль к косяку? var original = callback; callback = function() { if (sent) { original.apply(this, arguments); // это эквивалентно просто вызову callback с её штатными аргументами } else { var that = this, args = arguments; setTimeout(function() { // а это -- вызову callback после завершения текущего цикла корпоративной многозадачности, т.е. здесь -- заведомо не раньше, чем выполнится x.send(null); original.apply(that, args); }, 0); } } } */ function loaded() { if ( // XDomainRequest x.status === undefined || // modern browsers isSuccessful(x.status)) callback.call(x, null, x); else callback.call(x, x, null); } // Both `onreadystatechange` and `onload` can fire. `onreadystatechange` // has [been supported for longer](http://stackoverflow.com/a/9181508/229001). if ('onload' in x) { x.onload = loaded; } else { x.onreadystatechange = function readystate() { if (x.readyState === 4) { loaded(); } }; } // Call the callback with the XMLHttpRequest object as an error and prevent // it from ever being called again by reassigning it to `noop` x.onerror = function error(evt) { // XDomainRequest provides no evt parameter callback.call(this, evt || true, null); callback = function() { }; }; // IE9 must have onprogress be set to a unique function. x.onprogress = function() { }; x.ontimeout = function(evt) { callback.call(this, evt, null); callback = function() { }; }; x.onabort = function(evt) { callback.call(this, evt, null); callback = function() { }; }; // GET is the only supported HTTP Verb by XDomainRequest and is the // only one supported here. x.open('GET', url, true); // асинхронно //x.open('GET', url, true); // синхронно x.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0"); // иначе файл жестоко кешировался браузером, и никакого обновления не происходило // Send the request. Sending data is not supported. x.send(null); sent = true; return x; } if (typeof module !== 'undefined') module.exports = corslite; },{}],4:[function(require,module,exports){ 'use strict'; var dsv = require('d3-dsv'), sexagesimal = require('sexagesimal'); var latRegex = /(Lat)(itude)?/gi, lonRegex = /(L)(on|ng)(gitude)?/i; function guessHeader(row, regexp) { var name, match, score; for (var f in row) { match = f.match(regexp); if (match && (!name || match[0].length / f.length > score)) { score = match[0].length / f.length; name = f; } } return name; } function guessLatHeader(row) { return guessHeader(row, latRegex); } function guessLonHeader(row) { return guessHeader(row, lonRegex); } function isLat(f) { return !!f.match(latRegex); } function isLon(f) { return !!f.match(lonRegex); } function keyCount(o) { return (typeof o == 'object') ? Object.keys(o).length : 0; } function autoDelimiter(x) { var delimiters = [',', ';', '\t', '|']; var results = []; delimiters.forEach(function (delimiter) { var res = dsv.dsvFormat(delimiter).parse(x); if (res.length >= 1) { var count = keyCount(res[0]); for (var i = 0; i < res.length; i++) { if (keyCount(res[i]) !== count) return; } results.push({ delimiter: delimiter, arity: Object.keys(res[0]).length, }); } }); if (results.length) { return results.sort(function (a, b) { return b.arity - a.arity; })[0].delimiter; } else { return null; } } /** * Silly stopgap for dsv to d3-dsv upgrade * * @param {Array} x dsv output * @returns {Array} array without columns member */ function deleteColumns(x) { delete x.columns; return x; } function auto(x) { var delimiter = autoDelimiter(x); if (!delimiter) return null; return deleteColumns(dsv.dsvFormat(delimiter).parse(x)); } function csv2geojson(x, options, callback) { // text csv целиком, options, функция onparse как callback //console.log('[csv2geojson]',callback) if (!callback) { callback = options; options = {}; } options.delimiter = options.delimiter || ','; var latfield = options.latfield || '', lonfield = options.lonfield || '', crs = options.crs || ''; var features = [], featurecollection = {type: 'FeatureCollection', features: features}; if (crs !== '') { featurecollection.crs = {type: 'name', properties: {name: crs}}; } if (options.delimiter === 'auto' && typeof x == 'string') { options.delimiter = autoDelimiter(x); if (!options.delimiter) { callback({ type: 'Error', message: 'Could not autodetect delimiter' }); return; } } //console.log('leaflet-omnivore.js [csv2geojson] options.delimiter=',options.delimiter); // массив объектов из строк csv файла, начиная со второй, с именами атрибутов - из первой var parsed = (typeof x == 'string') ? dsv.dsvFormat(