635 lines
19 KiB
JavaScript
635 lines
19 KiB
JavaScript
|
(function(){
|
||
|
|
||
|
L.Control.LinearMeasurement = L.Control.extend({
|
||
|
|
||
|
options: {
|
||
|
position: 'topleft',
|
||
|
unitSystem: 'imperial', // imperial | metric
|
||
|
color: '#4D90FE',
|
||
|
contrastingColor: '#fff',
|
||
|
show_last_node: false,
|
||
|
show_azimut: false
|
||
|
},
|
||
|
|
||
|
clickSpeed: 300,
|
||
|
|
||
|
onAdd: function (map) {
|
||
|
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
|
||
|
link = L.DomUtil.create('a', 'icon-ruler', container),
|
||
|
map_container = map.getContainer(),
|
||
|
me = this;
|
||
|
|
||
|
link.href = '#';
|
||
|
link.title = 'Toggle measurement tool';
|
||
|
|
||
|
L.DomEvent.on(link, 'click', L.DomEvent.stop).on(link, 'click', function(){
|
||
|
if(L.DomUtil.hasClass(link, 'icon-active')){
|
||
|
me.resetRuler(!!me.mainLayer);
|
||
|
L.DomUtil.removeClass(link, 'icon-active');
|
||
|
L.DomUtil.removeClass(map_container, 'ruler-map');
|
||
|
|
||
|
} else {
|
||
|
me.initRuler();
|
||
|
L.DomUtil.addClass(link, 'icon-active');
|
||
|
L.DomUtil.addClass(map_container, 'ruler-map');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function contrastingColor(color){
|
||
|
return (luma(color) >= 165) ? '000' : 'fff';
|
||
|
}
|
||
|
|
||
|
function luma(color){
|
||
|
var rgb = (typeof color === 'string') ? hexToRGBArray(color) : color;
|
||
|
return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]); // SMPTE C, Rec. 709 weightings
|
||
|
}
|
||
|
|
||
|
function hexToRGBArray(color){
|
||
|
if (color.length === 3)
|
||
|
color = color.charAt(0) + color.charAt(0) + color.charAt(1) + color.charAt(1) + color.charAt(2) + color.charAt(2);
|
||
|
else if (color.length !== 6)
|
||
|
throw('Invalid hex color: ' + color);
|
||
|
var rgb = [];
|
||
|
for (var i = 0; i <= 2; i++)
|
||
|
rgb[i] = parseInt(color.substr(i * 2, 2), 16);
|
||
|
return rgb;
|
||
|
}
|
||
|
|
||
|
if(this.options.color && this.options.color.indexOf('#') === -1){
|
||
|
this.options.color = '#' + this.options.color;
|
||
|
} else if(!this.options.color){
|
||
|
this.options.color = '#4D90FE';
|
||
|
}
|
||
|
|
||
|
var originalColor = this.options.color.replace('#', '');
|
||
|
|
||
|
this.options.contrastingColor = '#'+contrastingColor(originalColor);
|
||
|
|
||
|
return container;
|
||
|
},
|
||
|
|
||
|
onRemove: function(map){
|
||
|
this.resetRuler(!!this.mainLayer);
|
||
|
},
|
||
|
|
||
|
initRuler: function(){
|
||
|
var me = this,
|
||
|
map = this._map;
|
||
|
|
||
|
this.mainLayer = L.featureGroup();
|
||
|
this.mainLayer.addTo(this._map);
|
||
|
|
||
|
map.touchZoom.disable();
|
||
|
map.doubleClickZoom.disable();
|
||
|
map.boxZoom.disable();
|
||
|
map.keyboard.disable();
|
||
|
|
||
|
if(map.tap) {
|
||
|
map.tap.disable();
|
||
|
}
|
||
|
|
||
|
this.dblClickEventFn = function(e){
|
||
|
L.DomEvent.stop(e);
|
||
|
};
|
||
|
|
||
|
this.clickEventFn = function(e){
|
||
|
if(me.clickHandle){
|
||
|
clearTimeout(me.clickHandle);
|
||
|
me.clickHandle = 0;
|
||
|
|
||
|
if(me.options.show_last_node){
|
||
|
me.preClick(e);
|
||
|
me.getMouseClickHandler(e);
|
||
|
}
|
||
|
|
||
|
me.getDblClickHandler(e);
|
||
|
|
||
|
} else {
|
||
|
me.preClick(e);
|
||
|
|
||
|
me.clickHandle = setTimeout(function(){
|
||
|
me.getMouseClickHandler(e);
|
||
|
me.clickHandle = 0;
|
||
|
|
||
|
}, me.clickSpeed);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.moveEventFn = function(e){
|
||
|
if(!me.clickHandle){
|
||
|
me.getMouseMoveHandler(e);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
map.on('click', this.clickEventFn, this);
|
||
|
map.on('mousemove', this.moveEventFn, this);
|
||
|
|
||
|
this.resetRuler();
|
||
|
},
|
||
|
|
||
|
initLayer: function(){
|
||
|
this.layer = L.featureGroup();
|
||
|
this.layer.addTo(this.mainLayer);
|
||
|
this.layer.on('selected', this.layerSelected);
|
||
|
this.layer.on('click', this.clickEventFn, this);
|
||
|
},
|
||
|
|
||
|
resetRuler: function(resetLayer){
|
||
|
var map = this._map;
|
||
|
|
||
|
if(resetLayer){
|
||
|
map.off('click', this.clickEventFn, this);
|
||
|
map.off('mousemove', this.moveEventFn, this);
|
||
|
|
||
|
if(this.mainLayer){
|
||
|
this._map.removeLayer(this.mainLayer);
|
||
|
}
|
||
|
|
||
|
this.mainLayer = null;
|
||
|
|
||
|
this._map.touchZoom.enable();
|
||
|
this._map.boxZoom.enable();
|
||
|
this._map.keyboard.enable();
|
||
|
|
||
|
if(this._map.tap) {
|
||
|
this._map.tap.enable();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.layer = null;
|
||
|
this.prevLatlng = null;
|
||
|
this.poly = null;
|
||
|
this.multi = null;
|
||
|
this.latlngs = null;
|
||
|
this.latlngsList = [];
|
||
|
this.sum = 0;
|
||
|
this.distance = 0;
|
||
|
this.separation = 1;
|
||
|
this.last = 0;
|
||
|
this.fixedLast = 0;
|
||
|
this.totalIcon = null;
|
||
|
this.total = null;
|
||
|
this.lastCircle = null;
|
||
|
|
||
|
/* Leaflet return distances in meters */
|
||
|
this.UNIT_CONV = 1000;
|
||
|
this.SUB_UNIT_CONV = 1000;
|
||
|
this.UNIT = 'km';
|
||
|
this.SUB_UNIT = 'm';
|
||
|
|
||
|
if(this.options.unitSystem === 'imperial'){
|
||
|
this.UNIT_CONV = 1609.344;
|
||
|
this.SUB_UNIT_CONV = 5280;
|
||
|
this.UNIT = 'mi';
|
||
|
this.SUB_UNIT = 'ft';
|
||
|
}
|
||
|
|
||
|
this.measure = {
|
||
|
scalar: 0,
|
||
|
unit: this.SUB_UNIT
|
||
|
};
|
||
|
},
|
||
|
|
||
|
cleanUpMarkers: function(fixed){
|
||
|
var layer = this.layer;
|
||
|
|
||
|
if(layer){
|
||
|
layer.eachLayer(function(l){
|
||
|
if(l.options && l.options.type === 'tmp'){
|
||
|
if(fixed){
|
||
|
l.options.type = 'fixed';
|
||
|
} else {
|
||
|
layer.removeLayer(l);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
cleanUpFixed: function(){
|
||
|
var layer = this.layer;
|
||
|
|
||
|
if(layer) {
|
||
|
layer.eachLayer(function(l){
|
||
|
if(l.options && (l.options.type === 'fixed')){
|
||
|
layer.removeLayer(l);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
convertDots: function(){
|
||
|
var me = this,
|
||
|
layer = this.layer;
|
||
|
|
||
|
if(layer) {
|
||
|
layer.eachLayer(function(l){
|
||
|
if(l.options && (l.options.type === 'dot')){
|
||
|
|
||
|
var m = l.options.marker,
|
||
|
i = m ? m.options.icon.options : null,
|
||
|
il = i ? i.html : '';
|
||
|
|
||
|
if(il && il.indexOf(me.measure.unit) === -1){
|
||
|
var str = l.options.label,
|
||
|
s = str.split(' '),
|
||
|
e = parseFloat(s[0]),
|
||
|
u = s[1],
|
||
|
label = '';
|
||
|
|
||
|
if(l.options.label.indexOf(me.measure.unit) !== -1){
|
||
|
label = l.options.label;
|
||
|
|
||
|
} else if(u === me.UNIT){
|
||
|
label = (e * me.SUB_UNIT_CONV).toFixed(2) + ' ' + me.SUB_UNIT;
|
||
|
|
||
|
} else if(u === me.SUB_UNIT){
|
||
|
label = (e / me.SUB_UNIT_CONV).toFixed(2) + ' ' + me.UNIT;
|
||
|
}
|
||
|
|
||
|
var cicon = L.divIcon({
|
||
|
className: 'total-popup-label',
|
||
|
html: label
|
||
|
});
|
||
|
|
||
|
m.setIcon(cicon);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
displayMarkers: function(latlngs, multi, sum) {
|
||
|
var x, y, label, ratio, p,
|
||
|
latlng = latlngs[latlngs.length-1],
|
||
|
prevLatlng = latlngs[0],
|
||
|
original = prevLatlng.distanceTo(latlng)/this.UNIT_CONV,
|
||
|
dis = original;
|
||
|
|
||
|
var p2 = this._map.latLngToContainerPoint(latlng),
|
||
|
p1 = this._map.latLngToContainerPoint(prevLatlng),
|
||
|
unit = 1;
|
||
|
|
||
|
if(this.measure.unit === this.SUB_UNIT){
|
||
|
unit = this.SUB_UNIT_CONV;
|
||
|
dis = dis * unit;
|
||
|
}
|
||
|
|
||
|
var t = (sum * unit) + dis,
|
||
|
qu = sum * unit;
|
||
|
|
||
|
for(var q = Math.floor(qu); q < t; q++){
|
||
|
ratio = (t-q) / dis;
|
||
|
|
||
|
if(q % this.separation || q < qu) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
x = (p2.x - ratio * (p2.x - p1.x));
|
||
|
y = (p2.y - ratio * (p2.y - p1.y));
|
||
|
|
||
|
p = L.point(x, y);
|
||
|
|
||
|
/* render a circle spaced by separation */
|
||
|
|
||
|
latlng = this._map.containerPointToLatLng(p);
|
||
|
|
||
|
label = (q + ' ' + this.measure.unit);
|
||
|
|
||
|
this.renderCircle(latlng, 0, this.layer, multi ? 'fixed' : 'tmp', label);
|
||
|
|
||
|
this.last = t;
|
||
|
}
|
||
|
|
||
|
return original;
|
||
|
},
|
||
|
|
||
|
renderCircle: function(latLng, radius, layer, type, label) {
|
||
|
var color = this.options.color,
|
||
|
lineColor = this.options.color,
|
||
|
azimut = '',
|
||
|
nodeCls = '';
|
||
|
|
||
|
type = type || 'circle';
|
||
|
|
||
|
linesHTML = [];
|
||
|
|
||
|
var options = {
|
||
|
color: lineColor,
|
||
|
fillOpacity: 1,
|
||
|
opacity: 1,
|
||
|
fill: true,
|
||
|
type: type
|
||
|
};
|
||
|
|
||
|
var a = this.prevLatlng ? this._map.latLngToContainerPoint(this.prevLatlng) : null,
|
||
|
b = this._map.latLngToContainerPoint(latLng);
|
||
|
|
||
|
if(type === 'dot'){
|
||
|
nodeCls = 'node-label';
|
||
|
|
||
|
if(a && this.options.show_azimut){
|
||
|
azimut = ' <span class="azimut"> '+this.lastAzimut+'°</span>';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
p_latLng = this._map.containerPointToLatLng(b);
|
||
|
|
||
|
if(label){
|
||
|
var cicon = L.divIcon({
|
||
|
className: 'total-popup-label ' + nodeCls,
|
||
|
html: '<span style="color: '+color+';">'+label+azimut+'</span>'
|
||
|
});
|
||
|
|
||
|
options.icon = cicon;
|
||
|
options.marker = L.marker(p_latLng, { icon: cicon, type: type }).addTo(layer);
|
||
|
options.label = label;
|
||
|
}
|
||
|
|
||
|
var circle = L.circleMarker(latLng, options);
|
||
|
|
||
|
circle.setRadius(3);
|
||
|
circle.addTo(layer);
|
||
|
|
||
|
return circle;
|
||
|
},
|
||
|
|
||
|
getAzimut: function(a, b){
|
||
|
var deg = 0;
|
||
|
|
||
|
if(a && b){
|
||
|
deg = parseInt(Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
|
||
|
|
||
|
if(deg > 0){
|
||
|
deg += 90;
|
||
|
} else if(deg < 0){
|
||
|
deg = Math.abs(deg);
|
||
|
if(deg <= 90){
|
||
|
deg = 90 - deg;
|
||
|
} else {
|
||
|
deg = 360 - (deg - 90);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.lastAzimut = deg;
|
||
|
|
||
|
return deg;
|
||
|
},
|
||
|
|
||
|
renderPolyline: function(latLngs, dashArray, layer) {
|
||
|
var poly = L.polyline(latLngs, {
|
||
|
color: this.options.color,
|
||
|
weight: 2,
|
||
|
opacity: 1,
|
||
|
dashArray: dashArray
|
||
|
});
|
||
|
|
||
|
poly.addTo(layer);
|
||
|
|
||
|
return poly;
|
||
|
},
|
||
|
|
||
|
renderMultiPolyline: function(latLngs, dashArray, layer) {
|
||
|
/* Leaflet version 1+ delegated the concept of multi-poly-line to the polyline */
|
||
|
var multi;
|
||
|
|
||
|
if(L.version.startsWith('0')){
|
||
|
multi = L.multiPolyline(latLngs, {
|
||
|
color: this.options.color,
|
||
|
weight: 2,
|
||
|
opacity: 1,
|
||
|
dashArray: dashArray
|
||
|
});
|
||
|
} else {
|
||
|
multi = L.polyline(latLngs, {
|
||
|
color: this.options.color,
|
||
|
weight: 2,
|
||
|
opacity: 1,
|
||
|
dashArray: dashArray
|
||
|
});
|
||
|
}
|
||
|
|
||
|
multi.addTo(layer);
|
||
|
|
||
|
return multi;
|
||
|
},
|
||
|
|
||
|
formatDistance: function(distance, precision) {
|
||
|
var s = L.Util.formatNum((distance < 1 ? distance*parseFloat(this.SUB_UNIT_CONV) : distance), precision),
|
||
|
u = (distance < 1 ? this.SUB_UNIT : this.UNIT);
|
||
|
|
||
|
return { scalar: s, unit: u };
|
||
|
},
|
||
|
|
||
|
hasClass: function(target, classes){
|
||
|
var fn = L.DomUtil.hasClass;
|
||
|
|
||
|
for(var i in classes){
|
||
|
if(fn(target, classes[i])){
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
preClick: function(e){
|
||
|
var me = this,
|
||
|
target = e.originalEvent.target;
|
||
|
|
||
|
if(this.hasClass(target, ['leaflet-popup', 'total-popup-content'])){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(!me.layer){
|
||
|
me.initLayer();
|
||
|
}
|
||
|
|
||
|
me.cleanUpMarkers(true);
|
||
|
|
||
|
me.fixedLast = me.last;
|
||
|
me.prevLatlng = e.latlng;
|
||
|
me.sum = 0;
|
||
|
},
|
||
|
|
||
|
getMouseClickHandler: function(e){
|
||
|
var me = this;
|
||
|
me.fixedLast = me.last;
|
||
|
me.sum = 0;
|
||
|
|
||
|
if(me.poly){
|
||
|
me.latlngsList.push(me.latlngs);
|
||
|
|
||
|
if(!me.multi){
|
||
|
me.multi = me.renderMultiPolyline(me.latlngsList, '5 5', me.layer, 'dot');
|
||
|
} else {
|
||
|
me.multi.setLatLngs(me.latlngsList);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var o, dis;
|
||
|
for(var l in me.latlngsList){
|
||
|
o = me.latlngsList[l];
|
||
|
me.sum += o[0].distanceTo(o[1])/me.UNIT_CONV;
|
||
|
}
|
||
|
|
||
|
if(me.measure.unit === this.SUB_UNIT){
|
||
|
dis = me.sum * me.SUB_UNIT_CONV;
|
||
|
} else {
|
||
|
dis = me.sum;
|
||
|
}
|
||
|
|
||
|
var s = dis.toFixed(2);
|
||
|
|
||
|
me.renderCircle(e.latlng, 0, me.layer, 'dot', parseInt(s) ? (s + ' ' + me.measure.unit) : '' );
|
||
|
me.prevLatlng = e.latlng;
|
||
|
},
|
||
|
|
||
|
getMouseMoveHandler: function(e){
|
||
|
var azimut = '';
|
||
|
|
||
|
if(this.prevLatlng){
|
||
|
var latLng = e.latlng;
|
||
|
|
||
|
this.latlngs = [this.prevLatlng, e.latlng];
|
||
|
|
||
|
if(!this.poly){
|
||
|
this.poly = this.renderPolyline(this.latlngs, '5 5', this.layer);
|
||
|
} else {
|
||
|
this.poly.setLatLngs(this.latlngs);
|
||
|
}
|
||
|
|
||
|
/* Distance in miles/meters */
|
||
|
this.distance = parseFloat(this.prevLatlng.distanceTo(e.latlng))/this.UNIT_CONV;
|
||
|
|
||
|
/* scalar and unit */
|
||
|
this.measure = this.formatDistance(this.distance + this.sum, 2);
|
||
|
|
||
|
var a = this.prevLatlng ? this._map.latLngToContainerPoint(this.prevLatlng) : null,
|
||
|
b = this._map.latLngToContainerPoint(latLng);
|
||
|
|
||
|
if(a && this.options.show_azimut){
|
||
|
var style = 'color: '+this.options.contrastingColor+';';
|
||
|
azimut = ' <span class="azimut azimut-final" style="'+style+'"> '+this.getAzimut(a, b)+'°</span>';
|
||
|
}
|
||
|
|
||
|
/* tooltip with total distance */
|
||
|
var label = this.measure.scalar + ' ' + this.measure.unit,
|
||
|
html = '<span class="total-popup-content" style="background-color:'+this.options.color+'; color: '+this.options.contrastingColor+'">' + label + azimut + '</span>';
|
||
|
|
||
|
if(!this.total){
|
||
|
this.totalIcon = L.divIcon({ className: 'total-popup', html: html });
|
||
|
|
||
|
this.total = L.marker(e.latlng, {
|
||
|
icon: this.totalIcon,
|
||
|
clickable: true
|
||
|
}).addTo(this.layer);
|
||
|
|
||
|
} else {
|
||
|
this.totalIcon = L.divIcon({ className: 'total-popup', html: html });
|
||
|
this.total.setLatLng(e.latlng);
|
||
|
this.total.setIcon(this.totalIcon);
|
||
|
}
|
||
|
|
||
|
/* Rules for separation using only distance criteria */
|
||
|
var ds = this.measure.scalar,
|
||
|
old_separation = this.separation,
|
||
|
digits = parseInt(ds).toString().length,
|
||
|
num = Math.pow(10, digits),
|
||
|
real = ds > (num/2) ? (num/10) : (num/20),
|
||
|
dimension = 0;
|
||
|
|
||
|
this.separation = real;
|
||
|
|
||
|
/* If there is a change in the segment length we want to re-space
|
||
|
the dots on the multi line */
|
||
|
if(old_separation !== this.separation && this.fixedLast){
|
||
|
this.cleanUpMarkers();
|
||
|
this.cleanUpFixed();
|
||
|
|
||
|
var multi_latlngs = this.multi.getLatLngs();
|
||
|
|
||
|
for(var s in multi_latlngs){
|
||
|
dimension += this.displayMarkers.apply(this, [multi_latlngs[s], true, dimension]);
|
||
|
}
|
||
|
|
||
|
this.displayMarkers.apply(this, [this.poly.getLatLngs(), false, this.sum]);
|
||
|
|
||
|
/* Review that the dots are in correct units */
|
||
|
this.convertDots();
|
||
|
|
||
|
} else {
|
||
|
this.cleanUpMarkers();
|
||
|
this.displayMarkers.apply(this, [this.poly.getLatLngs(), false, this.sum]);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
getDblClickHandler: function(e){
|
||
|
var azimut = '',
|
||
|
me = this;
|
||
|
|
||
|
if(!this.total){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.layer.off('click');
|
||
|
|
||
|
L.DomEvent.stop(e);
|
||
|
|
||
|
if(this.options.show_azimut){
|
||
|
var style = 'color: '+this.options.contrastingColor+';';
|
||
|
azimut = ' <span class="azimut azimut-final" style="'+style+'"> '+this.lastAzimut+'°</span>';
|
||
|
}
|
||
|
|
||
|
var workspace = this.layer,
|
||
|
label = this.measure.scalar + ' ' + this.measure.unit + ' ',
|
||
|
total_scalar = this.measure.unit === this.SUB_UNIT ? this.measure.scalar/this.UNIT_CONV : this.measure.scalar,
|
||
|
total_latlng = this.total.getLatLng(),
|
||
|
total_label = this.total,
|
||
|
html = [
|
||
|
'<div class="total-popup-content" style="background-color:'+this.options.color+'; color: '+this.options.contrastingColor+'">' + label + azimut,
|
||
|
' <svg class="close" viewbox="0 0 45 35">',
|
||
|
' <path style="stroke: '+this.options.contrastingColor+'" class="close" d="M 10,10 L 30,30 M 30,10 L 10,30" />',
|
||
|
' </svg>',
|
||
|
'</div>'
|
||
|
].join('');
|
||
|
|
||
|
this.totalIcon = L.divIcon({ className: 'total-popup', html: html });
|
||
|
this.total.setIcon(this.totalIcon);
|
||
|
|
||
|
var data = {
|
||
|
total: this.measure,
|
||
|
total_label: total_label,
|
||
|
unit: this.UNIT_CONV,
|
||
|
sub_unit: this.SUB_UNIT_CONV
|
||
|
};
|
||
|
|
||
|
var fireSelected = function(e){
|
||
|
if(L.DomUtil.hasClass(e.originalEvent.target, 'close')){
|
||
|
me.mainLayer.removeLayer(workspace);
|
||
|
} else {
|
||
|
workspace.fireEvent('selected', data);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
workspace.on('click', fireSelected);
|
||
|
workspace.fireEvent('selected', data);
|
||
|
|
||
|
this.resetRuler(false);
|
||
|
},
|
||
|
|
||
|
purgeLayers: function(layers){
|
||
|
for(var i in layers){
|
||
|
if(layers[i]) {
|
||
|
this.layer.removeLayer(layers[i]);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
layerSelected: function(e){}
|
||
|
});
|
||
|
|
||
|
})();
|