Skip to content

Commit

Permalink
Fixed #2626 fixed leaflet measure tool (#2730)
Browse files Browse the repository at this point in the history
* Fixed #2626 fixed leaflet measure tool

* added some comments to document changes

* improved implementation

* added more tests
  • Loading branch information
MV88 authored and Tobia Di Pisa committed Mar 14, 2018
1 parent 2a1eb06 commit 686b768
Show file tree
Hide file tree
Showing 7 changed files with 616 additions and 160 deletions.
277 changes: 244 additions & 33 deletions web/client/components/map/leaflet/MeasurementSupport.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,158 @@ const PropTypes = require('prop-types');
const React = require('react');
const assign = require('object-assign');
var L = require('leaflet');
const {slice} = require('lodash');
const {reproject, calculateAzimuth, calculateDistance, transformLineToArcs} = require('../../../utils/CoordinatesUtils');
const {
slice
} = require('lodash');
const {
reproject,
calculateAzimuth,
calculateDistance,
transformLineToArcs
} = require('../../../utils/CoordinatesUtils');
const {
convertUom,
getFormattedBearingValue
} = require('../../../utils/MeasureUtils');
require('leaflet-draw');

let defaultPrecision = {
km: 2,
ha: 2,
m: 2,
mi: 2,
ac: 2,
yd: 0,
ft: 0,
nm: 2,
sqkm: 2,
sqha: 2,
sqm: 2,
sqmi: 2,
sqac: 2,
sqyd: 2,
sqft: 2,
sqnm: 2
};


L.getMeasureWithTreshold = (value, threshold, source, dest, precision, sourceLabel, destLabel) => {
if (value > threshold) {
return L.GeometryUtil.formattedNumber(convertUom(value, source, dest), precision[dest]) + ' ' + destLabel;
}
return L.GeometryUtil.formattedNumber(value, precision[source]) + ' ' + sourceLabel;
};

/** @method readableDistance(distance, units): string
* Converts a metric distance to one of [ feet, nauticalMile, metric or yards ] string
*
* @alternative
* @method readableDistance(distance, isMetric, useFeet, isNauticalMile, precision, options): string
* Converts metric distance to distance string.
* The value will be rounded as defined by the precision option object.
* this override is necesary due to the customization on how the measure label is presented and for adding bearing support
*/

L.GeometryUtil.readableDistance = (distance, isMetric, isFeet, isNauticalMile, precision, options) => {
if (options.geomType === 'Bearing') {
return options.bearing;
}
let p = L.Util.extend({}, defaultPrecision, precision);
const {unit, label} = options.uom.length;

let distanceStr = L.GeometryUtil.formattedNumber(convertUom(distance, "m", unit), p[unit]) + ' ' + label;
if (options.useTreshold) {
if (isMetric) {
distanceStr = L.getMeasureWithTreshold(distance, 1000, "m", "km", p, "m", "km");
}
if (unit === "mi") {
distanceStr = L.getMeasureWithTreshold(convertUom(distance, "m", "yd"), 1760, "yd", "mi", p, "yd", "mi");
}
}
return distanceStr;
};

/** @method readableArea(area, isMetric, precision): string
* @return a readable area string in yards or metric.
* The value will be rounded as defined by the precision option object.
* this override is necesary due to the customization on how the area measure label is presented
supporting also the square nautical miles and square feets
*/
L.GeometryUtil.readableArea = (area, isMetric, precision, options) => {
const {unit, label} = options.uom.area;
let p = L.Util.extend({}, defaultPrecision, precision);
let areaStr = L.GeometryUtil.formattedNumber(convertUom(area, "sqm", unit), p[unit]) + ' ' + label;
if (options.useTreshold) { // this is done for retrocompatibility
if (isMetric) {
areaStr = L.getMeasureWithTreshold(area, 1000000, "sqm", "sqkm", p, "m²", "km²");
}
if (unit === "sqmi") {
areaStr = L.getMeasureWithTreshold(convertUom(area, "sqm", "sqyd"), 3097600, "sqyd", "sqmi", p, "yd²", "mi²");
}
}
return areaStr;
};

/**
* this is need to pass custom options as uom, useTreshold to the readableArea function
*/
L.Draw.Polygon.prototype._getMeasurementString = function() {
let area = this._area;
let measurementString = '';
if (!area && !this.options.showLength) {
return null;
}
if (this.options.showLength) {
measurementString = L.Draw.Polyline.prototype._getMeasurementString.call(this);
}

if (area) {
// here is the custom option passed to geom util func
const opt = {
uom: this.options.uom,
useTreshold: this.options.useTreshold
};
measurementString += this.options.showLength ? '<br>' : '' + L.GeometryUtil.readableArea(area, this.options.metric, this.options.precision, opt);
}
return measurementString;
};
/**
* this is need to pass custom options as uom, useTreshold, bearing to the readableDistance function
*/
L.Draw.Polyline.prototype._getMeasurementString = function() {
let currentLatLng = this._currentLatLng;
let previousLatLng = this._markers[this._markers.length - 1].getLatLng();
let distance;

// Calculate the distance from the last fixed point to the mouse position based on the version
if (L.GeometryUtil.isVersion07x()) {
distance = previousLatLng && currentLatLng && currentLatLng.distanceTo ? this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0;
} else {
distance = previousLatLng && currentLatLng ? this._measurementRunningTotal + this._map.distance(currentLatLng, previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0;
}
// here is the custom option passed to geom util func
const opt = {
uom: this.options.uom,
useTreshold: this.options.useTreshold,
geomType: this.options.geomType,
bearing: this.options.bearing ? getFormattedBearingValue(this.options.bearing) : 0
};
return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic, this.options.precision, opt);
};
class MeasurementSupport extends React.Component {
static displayName = 'MeasurementSupport';

static propTypes = {
map: PropTypes.object,
metric: PropTypes.bool,
feet: PropTypes.bool,
nautic: PropTypes.bool,
useTreshold: PropTypes.bool,
projection: PropTypes.string,
measurement: PropTypes.object,
changeMeasurementState: PropTypes.func,
messages: PropTypes.object,
uom: PropTypes.object,
updateOnMouseMove: PropTypes.bool
};

Expand All @@ -25,20 +162,34 @@ class MeasurementSupport extends React.Component {
};

static defaultProps = {
uom: {
length: {unit: 'm', label: 'm'},
area: {unit: 'sqm', label: 'm²'}
},
updateOnMouseMove: false,
metric: true,
nautic: false,
useTreshold: false,
feet: false
};


componentWillReceiveProps(newProps) {
if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType ) {
if ((newProps && newProps.uom && newProps.uom.length && newProps.uom.length.unit) !== (this.props && this.props.uom && this.props.uom.length && this.props.uom.length.unit) && this.drawControl) {
const uomOptions = this.uomLengthOptions(newProps);
this.drawControl.setOptions({...uomOptions, uom: newProps.uom});
}
if ((newProps && newProps.uom && newProps.uom.area && newProps.uom.area.unit) !== (this.props && this.props.uom && this.props.uom.area && this.props.uom.area.unit) && this.drawControl) {
const uomOptions = this.uomAreaOptions(newProps);
this.drawControl.setOptions({...uomOptions, uom: newProps.uom});
}
if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType) {
this.addDrawInteraction(newProps);
}
if (!newProps.measurement.geomType) {
this.removeDrawInteraction();
}
}

onDrawStart = () => {
this.removeArcLayer();
this.drawing = true;
Expand All @@ -54,11 +205,20 @@ class MeasurementSupport extends React.Component {
let feature = this.lastLayer && this.lastLayer.toGeoJSON() || {};
if (this.props.measurement.geomType === 'Point') {
let pos = this.drawControl._marker.getLatLng();
let point = {x: pos.lng, y: pos.lat, srs: 'EPSG:4326'};
let newMeasureState = assign({}, this.props.measurement, {point: point, feature});
let point = {
x: pos.lng,
y: pos.lat,
srs: 'EPSG:4326'
};
let newMeasureState = assign({}, this.props.measurement, {
point: point,
feature
});
this.props.changeMeasurementState(newMeasureState);
} else {
let newMeasureState = assign({}, this.props.measurement, {feature});
let newMeasureState = assign({}, this.props.measurement, {
feature
});
this.props.changeMeasurementState(newMeasureState);
}
if (this.props.measurement.lineMeasureEnabled && this.lastLayer) {
Expand Down Expand Up @@ -114,7 +274,7 @@ class MeasurementSupport extends React.Component {
if (this.props.measurement.geomType === 'LineString' && this.drawControl._markers && this.drawControl._markers.length > 1) {
// calculate length
const reprojectedCoords = this.drawControl._markers.reduce((p, c) => {
const {lng, lat} = c.getLatLng();
const { lng, lat } = c.getLatLng();
return [...p, [lng, lat]];
}, []);

Expand All @@ -126,29 +286,15 @@ class MeasurementSupport extends React.Component {
area = L.GeometryUtil.geodesicArea(latLngs);
} else if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) {
// calculate bearing
let bearingMarkers = this.drawControl._markers;
let coords1 = [bearingMarkers[0].getLatLng().lng, bearingMarkers[0].getLatLng().lat];
let coords2;
if (bearingMarkers.length === 1) {
coords2 = [currentLatLng.lng, currentLatLng.lat];
} else if (bearingMarkers.length === 2) {
coords2 = [bearingMarkers[1].getLatLng().lng, bearingMarkers[1].getLatLng().lat];
}
// in order to align the results between leaflet and openlayers the coords are repojected only for leaflet
coords1 = reproject(coords1, 'EPSG:4326', this.props.projection);
coords2 = reproject(coords2, 'EPSG:4326', this.props.projection);
// calculate the azimuth as base for bearing information
bearing = calculateAzimuth(coords1, coords2, this.props.projection);
bearing = this.calculateBearing();
}
// let drawn geom stay on the map
let newMeasureState = assign({}, this.props.measurement,
{
point: null, // Point is set in onDraw.created
len: distance,
area: area,
bearing: bearing
}
);
let newMeasureState = assign({}, this.props.measurement, {
point: null, // Point is set in onDraw.created
len: distance,
area: area,
bearing: bearing
});
this.props.changeMeasurementState(newMeasureState);
};

Expand Down Expand Up @@ -182,23 +328,28 @@ class MeasurementSupport extends React.Component {
this.props.map.on('draw:created', this.onDrawCreated, this);
this.props.map.on('draw:drawstart', this.onDrawStart, this);
this.props.map.on('click', this.mapClickHandler, this);
this.props.map.on('mousemove', this.updateBearing, this);

if (this.props.updateOnMouseMove) {
this.props.map.on('mousemove', this.updateMeasurementResults, this);
}

if (newProps.measurement.geomType === 'Point') {
this.drawControl = new L.Draw.Marker(this.props.map, {
repeatMode: false
});
} else if (newProps.measurement.geomType === 'LineString' ||
newProps.measurement.geomType === 'Bearing') {
newProps.measurement.geomType === 'Bearing') {
const uomOptions = this.uomLengthOptions(newProps);
this.drawControl = new L.Draw.Polyline(this.props.map, {
shapeOptions: {
color: '#ffcc33',
weight: 2
},
metric: this.props.metric,
feet: this.props.feet,
showLength: true,
useTreshold: newProps.useTreshold,
uom: newProps.uom,
geomType: newProps.measurement.geomType,
...uomOptions,
repeatMode: false,
icon: new L.DivIcon({
iconSize: new L.Point(8, 8),
Expand All @@ -210,13 +361,21 @@ class MeasurementSupport extends React.Component {
})
});
} else if (newProps.measurement.geomType === 'Polygon') {
const uomOptions = this.uomAreaOptions(newProps);
this.drawControl = new L.Draw.Polygon(this.props.map, {
shapeOptions: {
color: '#ffcc33',
weight: 2,
fill: 'rgba(255, 255, 255, 0.2)'
},
showArea: true,
allowIntersection: false,
showLength: false,
repeatMode: false,
useTreshold: newProps.useTreshold,
uom: newProps.uom,
geomType: newProps.measurement.geomType,
...uomOptions,
icon: new L.DivIcon({
iconSize: new L.Point(8, 8),
className: 'leaflet-div-icon leaflet-editing-icon'
Expand All @@ -239,6 +398,7 @@ class MeasurementSupport extends React.Component {
this.removeLastLayer();
this.props.map.off('draw:created', this.onDrawCreated, this);
this.props.map.off('draw:drawstart', this.onDrawStart, this);
this.props.map.off('mousemove', this.updateBearing, this);
this.props.map.off('click', this.mapClickHandler, this);
if (this.props.updateOnMouseMove) {
this.props.map.off('mousemove', this.updateMeasurementResults, this);
Expand All @@ -255,6 +415,57 @@ class MeasurementSupport extends React.Component {
this.props.map.removeLayer(this.arcLayer);
}
}

uomLengthOptions = (props) => {
let {
unit
} = props.uom.length;
const metric = unit === "m" || unit === "km"; // false = miles&yards
const nautic = unit === "nm";
const feet = unit === "ft";
return {
metric,
nautic,
feet
};
}
uomAreaOptions = (props) => {
let {
unit
} = props.uom.area;
const metric = unit === "sqm" || unit === "sqkm"; // false = miles
const nautic = unit === "sqnm";
const feet = unit === "sqft";
return {
metric,
nautic,
feet
};
}

calculateBearing = () => {
let currentLatLng = this.drawControl._currentLatLng;
let bearing = 0;
let bearingMarkers = this.drawControl._markers;
let coords1 = [bearingMarkers[0].getLatLng().lng, bearingMarkers[0].getLatLng().lat];
let coords2;
if (bearingMarkers.length === 1) {
coords2 = [currentLatLng.lng, currentLatLng.lat];
} else if (bearingMarkers.length === 2) {
coords2 = [bearingMarkers[1].getLatLng().lng, bearingMarkers[1].getLatLng().lat];
}
// in order to align the results between leaflet and openlayers the coords are repojected only for leaflet
coords1 = reproject(coords1, 'EPSG:4326', this.props.projection);
coords2 = reproject(coords2, 'EPSG:4326', this.props.projection);
// calculate the azimuth as base for bearing information
bearing = calculateAzimuth(coords1, coords2, this.props.projection);
return bearing;
}
updateBearing = () => {
if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) {
this.drawControl.setOptions({ bearing: this.calculateBearing() });
}
}
}

module.exports = MeasurementSupport;
Loading

0 comments on commit 686b768

Please sign in to comment.