diff --git a/web/client/actions/__tests__/measurement-test.js b/web/client/actions/__tests__/measurement-test.js index 89c3d8a61a..18ccf944db 100644 --- a/web/client/actions/__tests__/measurement-test.js +++ b/web/client/actions/__tests__/measurement-test.js @@ -16,7 +16,8 @@ const { init, INIT, changeGeometry, CHANGED_GEOMETRY, changeCoordinates, CHANGE_COORDINATES, - addAnnotation, ADD_MEASURE_AS_ANNOTATION + addAnnotation, ADD_MEASURE_AS_ANNOTATION, + updateMeasures, UPDATE_MEASURES } = require('../measurement'); const feature = {type: "Feature", geometry: { coordinates: [], @@ -113,4 +114,10 @@ describe('Test correctness of measurement actions', () => { expect(retval.type).toBe(CHANGE_COORDINATES); expect(retval.coordinates).toEqual(coordinates); }); + it('Test updateMeasures action creator', () => { + const retval = updateMeasures({len: 0}); + expect(retval).toExist(); + expect(retval.type).toBe(UPDATE_MEASURES); + expect(retval.measures).toEqual({len: 0}); + }); }); diff --git a/web/client/actions/measurement.js b/web/client/actions/measurement.js index 3fa6f0dbf7..b2e4a823b8 100644 --- a/web/client/actions/measurement.js +++ b/web/client/actions/measurement.js @@ -13,6 +13,7 @@ const RESET_GEOMETRY = 'MEASUREMENT:RESET_GEOMETRY'; const CHANGE_FORMAT = 'MEASUREMENT:CHANGE_FORMAT'; const CHANGE_COORDINATES = 'MEASUREMENT:CHANGE_COORDINATES'; const ADD_MEASURE_AS_ANNOTATION = 'MEASUREMENT:ADD_MEASURE_AS_ANNOTATION'; +const UPDATE_MEASURES = 'MEASUREMENT:UPDATE_MEASURES'; const INIT = 'MEASUREMENT:INIT'; /** @@ -79,6 +80,12 @@ function resetGeometry() { type: RESET_GEOMETRY }; } +function updateMeasures(measures) { + return { + type: UPDATE_MEASURES, + measures + }; +} function changeMeasurementState(measureState) { return { type: CHANGE_MEASUREMENT_STATE, @@ -109,6 +116,7 @@ module.exports = { changeUom, CHANGE_UOM, changeGeometry, CHANGED_GEOMETRY, changeFormatMeasurement, CHANGE_FORMAT, + updateMeasures, UPDATE_MEASURES, changeCoordinates, CHANGE_COORDINATES, resetGeometry, RESET_GEOMETRY, addAnnotation, ADD_MEASURE_AS_ANNOTATION, diff --git a/web/client/components/map/openlayers/DrawSupport.jsx b/web/client/components/map/openlayers/DrawSupport.jsx index 1dc73328d2..e4292c4158 100644 --- a/web/client/components/map/openlayers/DrawSupport.jsx +++ b/web/client/components/map/openlayers/DrawSupport.jsx @@ -379,7 +379,7 @@ class DrawSupport extends React.Component { this.drawInteraction.on('drawend', function(evt) { this.sketchFeature = evt.feature; this.sketchFeature.set('id', uuid.v1()); - let drawnGeom = evt.feature.getGeometry(); + let drawnGeom = this.sketchFeature.getGeometry(); let drawnFeatures = this.drawLayer.getSource().getFeatures(); let previousGeometries; let features = this.props.features; @@ -395,7 +395,7 @@ class DrawSupport extends React.Component { newFeature = this.getNewFeature(newDrawMethod, coordinates); // TODO verify center is projected in 4326 and is an array center = reproject(center, this.getMapCrs(), "EPSG:4326", false); - const originalId = newProps && newProps.features && newProps.features.length && newProps.features[0].features && newProps.features[0].features.length && newProps.features[0].features.filter(f => f.properties.isDrawing)[0].properties.id || this.sketchFeature.get("id"); + const originalId = newProps && newProps.features && newProps.features.length && newProps.features[0] && newProps.features[0].features && newProps.features[0].features.length && newProps.features[0].features.filter(f => f.properties.isDrawing)[0].properties.id || this.sketchFeature.get("id"); // this.sketchFeature.set('id', originalId); newFeature.setProperties({isCircle: true, radius, center: [center.x, center.y], id: originalId}); } else if (drawMethod === "Polygon") { @@ -435,7 +435,7 @@ class DrawSupport extends React.Component { const center = drawnGeom.getCenter(); const coordinates = this.polygonCoordsFromCircle(center, radius); const newMultiGeom = this.toMulti(this.createOLGeometry({type: newDrawMethod, coordinates})); - if (features.length === 1 && !features[0].geometry) { + if (features.length === 1 && features[0] && !features[0].geometry) { previousGeometries = []; geomCollection = new ol.geom.GeometryCollection([newMultiGeom]); } else { @@ -730,8 +730,8 @@ class DrawSupport extends React.Component { this.addFeatures(newProps); } }; - addSingleClickListener = (singleclickCallback) => { - let evtKey = this.props.map.on('singleclick', singleclickCallback); + addSingleClickListener = (singleclickCallback, props) => { + let evtKey = props.map.on('singleclick', singleclickCallback); return evtKey; }; @@ -740,7 +740,7 @@ class DrawSupport extends React.Component { if (this.state && this.state.keySingleClickCallback) { ol.Observable.unByKey(this.state.keySingleClickCallback); } - const singleClickCallback = (e) => { + const singleClickCallback = (event) => { if (this.drawSource && newProps.options) { let previousFeatures = this.drawSource.getFeatures(); let previousFtIndex = 0; @@ -761,22 +761,22 @@ class DrawSupport extends React.Component { if (isCompletePolygon(previousCoords)) { // insert at penultimate position actualCoords = slice(previousCoords[0], 0, previousCoords[0].length - 1); - actualCoords = actualCoords.concat([e.coordinate]); + actualCoords = actualCoords.concat([event.coordinate]); actualCoords = [actualCoords.concat([previousCoords[0][0]])]; } else { // insert at ultimate position if more than 2 point - actualCoords = previousCoords[0].length > 1 ? [[...previousCoords[0], e.coordinate, previousCoords[0][0] ]] : [[...previousCoords[0], e.coordinate ]]; + actualCoords = previousCoords[0].length > 1 ? [[...previousCoords[0], event.coordinate, previousCoords[0][0] ]] : [[...previousCoords[0], event.coordinate ]]; } } else { // insert at first position - actualCoords = [[e.coordinate]]; + actualCoords = [[event.coordinate]]; } olFt = this.getNewFeature(newDrawMethod, actualCoords); olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry")); break; } case "LineString": case "MultiPoint": { - actualCoords = previousCoords.length ? [...previousCoords, e.coordinate] : [e.coordinate]; + actualCoords = previousCoords.length ? [...previousCoords, event.coordinate] : [event.coordinate]; olFt = this.getNewFeature(newDrawMethod, actualCoords); olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry")); } @@ -784,7 +784,7 @@ class DrawSupport extends React.Component { case "Circle": { newDrawMethod = "Polygon"; const radius = previousFt && previousFt.getProperties() && previousFt.getProperties().radius || 10000; - let center = e.coordinate; + let center = event.coordinate; const coords = this.polygonCoordsFromCircle(center, 100); olFt = this.getNewFeature(newDrawMethod, coords); // TODO verify center is projected in 4326 and is an array @@ -795,14 +795,14 @@ class DrawSupport extends React.Component { } case "Text": { newDrawMethod = "Point"; - olFt = this.getNewFeature(newDrawMethod, e.coordinate); + olFt = this.getNewFeature(newDrawMethod, event.coordinate); olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry")); olFt.setProperties({isText: true, valueText: previousFt && previousFt.getProperties() && previousFt.getProperties().valueText || newProps.options.defaultTextAnnotation || "New" }); break; } // point default: { - actualCoords = e.coordinate; + actualCoords = event.coordinate; olFt = this.getNewFeature(newDrawMethod, actualCoords); olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry")); } @@ -835,7 +835,7 @@ class DrawSupport extends React.Component { }; this.clean(); - let newFeature = reprojectGeoJson(head(newProps.features), newProps.options.featureProjection, this.getMapCrs()); + let newFeature = reprojectGeoJson(head(newProps.features), newProps.options.featureProjection, this.getMapCrs()) || {}; let props; if (newFeature && newFeature.features && newFeature.features.length) { // filtering circles features only when drawing @@ -864,7 +864,7 @@ class DrawSupport extends React.Component { this.addTranslateInteraction(); } if (newProps.options.addClickCallback) { - this.setState({keySingleClickCallback: this.addSingleClickListener(singleClickCallback)}); + this.setState({keySingleClickCallback: this.addSingleClickListener(singleClickCallback, newProps)}); } } if (newProps.options && newProps.options.selectEnabled) { @@ -999,7 +999,7 @@ class DrawSupport extends React.Component { // retrieve geodesic center from properties // it's different from extent center let center = geometryProperties && geometryProperties.geodesicCenter || ol.extent.getCenter(extent); - let coordinates = geometry.getCoordinates(); + let coordinates; let projection = this.props.map.getView().getProjection().getCode(); let radius; let type = geometry.getType(); diff --git a/web/client/components/map/openlayers/Feature.jsx b/web/client/components/map/openlayers/Feature.jsx index b7f06cc29f..fe0b6945ef 100644 --- a/web/client/components/map/openlayers/Feature.jsx +++ b/web/client/components/map/openlayers/Feature.jsx @@ -40,19 +40,13 @@ class Feature extends React.Component { // TODO check if shallow comparison is enough properties and geometry return !isEqual(nextProps.properties, this.props.properties) || !isEqual(nextProps.geometry, this.props.geometry) || - (nextProps.features !== this.props.features) || - (nextProps.style !== this.props.style); + !isEqual(nextProps.features, this.props.features) || + !isEqual(nextProps.style, this.props.style); } componentWillUpdate(nextProps) { - // TODO check if shallow comparison is enough properties and geometry - if (!isEqual(nextProps.properties, this.props.properties) || - !isEqual(nextProps.geometry, this.props.geometry) || - (nextProps.features !== this.props.features) || - (nextProps.style !== this.props.style)) { - this.removeFromContainer(); - this.addFeatures(nextProps); - } + this.removeFromContainer(); + this.addFeatures(nextProps); } componentWillUnmount() { diff --git a/web/client/components/map/openlayers/LegacyVectorStyle.js b/web/client/components/map/openlayers/LegacyVectorStyle.js index 21606eda61..ef77f98c4f 100644 --- a/web/client/components/map/openlayers/LegacyVectorStyle.js +++ b/web/client/components/map/openlayers/LegacyVectorStyle.js @@ -4,7 +4,7 @@ var ol = require('openlayers'); const {last, head} = require('lodash'); const blue = [0, 153, 255, 1]; const assign = require('object-assign'); -const {trim, isString} = require('lodash'); +const {trim, isString, isArray} = require('lodash'); const {colorToRgbaStr} = require('../../../utils/ColorUtils'); const {set} = require('../../../utils/ImmutableUtils'); const selectedStyleConfiguration = { @@ -485,7 +485,11 @@ function getStyle(options, isDrawing = false, textValues = []) { })); } let gStyle = getValidStyle(type, options, isDrawing, textValues); - gStyle.setGeometry(c); + if (isArray(gStyle)) { + gStyle.forEach(s => s.setGeometry(c)); + } else { + gStyle.setGeometry(c); + } return p.concat([gStyle]); }, []); return styles; diff --git a/web/client/components/map/openlayers/MeasurementSupport.jsx b/web/client/components/map/openlayers/MeasurementSupport.jsx index 69fb7ad086..41cffd1d0e 100644 --- a/web/client/components/map/openlayers/MeasurementSupport.jsx +++ b/web/client/components/map/openlayers/MeasurementSupport.jsx @@ -8,25 +8,30 @@ const React = require('react'); const PropTypes = require('prop-types'); -const {round, isEqual, dropRight, slice} = require('lodash'); +const {round, isEqual, dropRight, pick} = require('lodash'); const assign = require('object-assign'); const ol = require('openlayers'); const wgs84Sphere = new ol.Sphere(6378137); const {reprojectGeoJson, reproject, calculateAzimuth, calculateDistance, transformLineToArcs} = require('../../../utils/CoordinatesUtils'); -const {convertUom, getFormattedBearingValue, isValidGeometry} = require('../../../utils/MeasureUtils'); +const {convertUom, getFormattedBearingValue} = require('../../../utils/MeasureUtils'); const {set} = require('../../../utils/ImmutableUtils'); const {startEndPolylineStyle} = require('./VectorStyle'); const {getMessageById} = require('../../../utils/LocaleUtils'); +const {createOLGeometry} = require('../../../utils/openlayers/DrawUtils'); + +const getProjectionCode = (olMap) => { + return olMap.getView().getProjection().getCode(); +}; class MeasurementSupport extends React.Component { static propTypes = { startEndPoint: PropTypes.object, map: PropTypes.object, - projection: PropTypes.string, measurement: PropTypes.object, enabled: PropTypes.bool, uom: PropTypes.object, changeMeasurementState: PropTypes.func, + updateMeasures: PropTypes.func, resetGeometry: PropTypes.func, changeGeometry: PropTypes.func, updateOnMouseMove: PropTypes.bool @@ -37,6 +42,10 @@ class MeasurementSupport extends React.Component { }; static defaultProps = { + changeMeasurementState: () => {}, + resetGeometry: () => {}, + updateMeasures: () => {}, + changeGeometry: () => {}, startEndPoint: { startPointOptions: { radius: 3, @@ -50,8 +59,10 @@ class MeasurementSupport extends React.Component { updateOnMouseMove: false }; + /** + * we assume that only valid features are passed to the draw tools + */ componentWillReceiveProps(newProps) { - this.invalidCoordinates = []; if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType || /* check also when a measure tool is enabled * if so the first condition does not match @@ -64,56 +75,30 @@ class MeasurementSupport extends React.Component { if (!newProps.measurement.geomType) { this.removeDrawInteraction(); } - /** - * if invalid coords are sent here just or draw only valid ones - */ - let ft = newProps.measurement.feature; let oldFt = this.props.measurement.feature; - let isNewGeomPresent = ft && ft.geometry; - if (isNewGeomPresent && !isEqual(ft.geometry, oldFt.geometry) && !isValidGeometry(ft.geometry)) { - // filtering out the invalid coords because causing errors/crash when reprojecting - let props = set("measurement.feature.geometry.coordinates", (ft.geometry.type === "Polygon" ? ft.geometry.coordinates[0] : ft.geometry.coordinates) - .filter((c, i) => { - const isValid = !isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1])); - if (!isValid) { - // if some coords are invalid we then need to add it to the feature before updating the state - this.invalidCoordinates.push({coord: c, index: i}); - } - return isValid; - } - ), newProps); - if (ft.geometry.type === "Polygon") { - props = set("measurement.feature.geometry.coordinates", [props.measurement.feature.geometry.coordinates], props); - } - this.updateFeatures(props); - } + let newFt = newProps.measurement.feature; /** - * this is needed to update the feature drawn by this tool and recalculate the measures, - * although the recalculation should be moved in the reducer. so we can avoid to to weird stuff here if there is another plugin / component - * that is going to update the coordinates via UI or any other action + * update the feature drawn and recalculate the measures and tooltips + * then update the stae with only the new measures calculated */ - if (isNewGeomPresent && isValidGeometry(ft.geometry) && newProps.measurement.updatedByUI && (!isEqual(oldFt, ft) || !isEqual(this.props.uom, newProps.uom))) { - this.updateFeatures(newProps); + if (newFt && newFt.geometry && newProps.measurement.updatedByUI && (!isEqual(oldFt, newFt) || !isEqual(this.props.uom, newProps.uom))) { + this.updateMeasures(newProps); } } - getPointCoordinate = (coordinate) => { - return reproject(coordinate, this.props.projection, 'EPSG:4326'); - }; - render() { return null; } - isPolygon = (feature) => { - return feature.geometry.type === "Polygon"; + validateCoords = (coords) => { + return coords.filter((c) => !isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1]))); } /** * This method takes the feature from properties and * it updated the drawn feature and its measure tooltip * It must receive only valid coordinates */ - updateFeatures = (props) => { + updateMeasures = (props) => { this.replaceFeatures([props.measurement.feature], props); // update measure tooltip @@ -158,38 +143,15 @@ class MeasurementSupport extends React.Component { } this.source = new ol.source.Vector(); featuresToReplace.forEach((geoJSON) => { - let geoJSONFT = geoJSON; - // polygons must have at least 4 points where the first being equal to the last - if (geoJSONFT.geometry.type === "Polygon" && geoJSONFT.geometry.coordinates[0].length >= 3) { - if (!isEqual(geoJSONFT.geometry.coordinates[0][0], geoJSONFT.geometry.coordinates[0][geoJSONFT.geometry.coordinates[0].length - 1])) { - geoJSONFT = set("geometry.coordinates", [geoJSONFT.geometry.coordinates[0].concat([geoJSONFT.geometry.coordinates[0][0]])], geoJSONFT); - } - } - let geometry = reprojectGeoJson(geoJSONFT, "EPSG:4326", this.props.map.getView().getProjection().getCode()).geometry; + let geometry = reprojectGeoJson(geoJSON, "EPSG:4326", getProjectionCode(this.props.map)).geometry; const feature = new ol.Feature({ - geometry: this.createOLGeometry(geometry) + geometry: createOLGeometry(geometry) }); this.source.addFeature(feature); }); this.measureLayer.setSource(this.source); }; - createOLGeometry = ({type, coordinates, radius, center}) => { - let geometry; - switch (type) { - case "Point": { geometry = new ol.geom.Point(coordinates ? coordinates : []); break; } - case "LineString": { geometry = new ol.geom.LineString(coordinates ? coordinates : []); break; } - case "MultiPoint": { geometry = new ol.geom.MultiPoint(coordinates ? coordinates : []); break; } - case "MultiLineString": { geometry = new ol.geom.MultiLineString(coordinates ? coordinates : []); break; } - case "MultiPolygon": { geometry = new ol.geom.MultiPolygon(coordinates ? coordinates : []); break; } - // defaults is Polygon - default: { geometry = radius && center ? - ol.geom.Polygon.fromCircle(new ol.geom.Circle([center.x, center.y], radius), 100) : new ol.geom.Polygon(coordinates ? coordinates : []); - } - } - return geometry; - }; - addDrawInteraction = (newProps) => { let vector; let draw; @@ -308,7 +270,7 @@ class MeasurementSupport extends React.Component { draw.on('drawend', function(evt) { this.drawing = false; const geojsonFormat = new ol.format.GeoJSON(); - let newFeature = reprojectGeoJson(geojsonFormat.writeFeatureObject(evt.feature.clone()), this.props.map.getView().getProjection().getCode(), "EPSG:4326"); + let newFeature = reprojectGeoJson(geojsonFormat.writeFeatureObject(evt.feature.clone()), getProjectionCode(this.props.map), "EPSG:4326"); this.props.changeGeometry(newFeature); if (this.props.measurement.lineMeasureEnabled) { // Calculate arc @@ -388,65 +350,61 @@ class MeasurementSupport extends React.Component { if (props.measurement.geomType === 'Bearing' && sketchCoords.length > 1) { // calculate the azimuth as base for bearing information - bearing = calculateAzimuth(sketchCoords[0], sketchCoords[1], props.projection); + bearing = calculateAzimuth(sketchCoords[0], sketchCoords[1], getProjectionCode(props.map)); if (sketchCoords.length > 2) { this.drawInteraction.sketchCoords_ = [sketchCoords[0], sketchCoords[1], sketchCoords[0]]; + while (this.sketchFeature.getGeometry().getCoordinates().length > 3) { + /* + * In some cases, if the user is too quick changing direction after the creation of the second point + * (before the draw interaction stops) new point are created in the interaction and have to be removed + * note: `> 3` is because the last point of the sketchFeature is the current mouse position + */ + this.drawInteraction.removeLastPoint(); + } this.sketchFeature.getGeometry().setCoordinates([sketchCoords[0], sketchCoords[1]]); this.drawInteraction.sketchFeature_ = this.sketchFeature; this.drawInteraction.finishDrawing(); } } const geojsonFormat = new ol.format.GeoJSON(); - let feature = reprojectGeoJson(geojsonFormat.writeFeatureObject(this.sketchFeature.clone()), props.map.getView().getProjection().getCode(), "EPSG:4326"); + let feature = reprojectGeoJson(geojsonFormat.writeFeatureObject(this.sketchFeature.clone()), getProjectionCode(props.map), "EPSG:4326"); // it will no longer create 100 points for arcs to put in the state let newMeasureState = assign({}, props.measurement, { - point: props.measurement.geomType === 'Point' ? this.getPointCoordinate(sketchCoords) : null, - len: props.measurement.geomType === 'LineString' ? calculateDistance(this.reprojectedCoordinates(sketchCoords), props.measurement.lengthFormula) : 0, + point: props.measurement.geomType === 'Point' ? reproject(sketchCoords, getProjectionCode(this.props.map), 'EPSG:4326') : null, + len: props.measurement.geomType === 'LineString' ? calculateDistance(this.reprojectedCoordinatesIn4326(sketchCoords), props.measurement.lengthFormula) : 0, area: props.measurement.geomType === 'Polygon' ? this.calculateGeodesicArea(this.sketchFeature.getGeometry().getLinearRing(0).getCoordinates()) : 0, bearing: props.measurement.geomType === 'Bearing' ? bearing : 0, lenUnit: props.measurement.lenUnit, areaUnit: props.measurement.areaUnit, - updatedByUI - }, - // feature should not change if the changes comes from ui - !updatedByUI ? { feature: set("geometry.coordinates", this.drawing ? props.measurement.geomType === 'Polygon' ? [dropRight(feature.geometry.coordinates[0], feature.geometry.coordinates[0].length <= 2 ? 0 : 1)] : dropRight(feature.geometry.coordinates) : feature.geometry.coordinates, feature) - } : {} - ); - // checks for invalid coords that needs to be re-added - if (this.invalidCoordinates.length) { - let newCoords; - this.invalidCoordinates.forEach(c => { - let coords = props.measurement.geomType === 'Polygon' ? newMeasureState.feature.geometry.coordinates[0] : newMeasureState.feature.geometry.coordinates; - newCoords = slice(coords, 0, c.index); - newCoords = newCoords.concat([c.coord]); - newCoords = newCoords.concat(slice(coords, c.index, coords.length)); - newMeasureState = set("feature.geometry.coordinates", props.measurement.geomType === 'Polygon' ? [newCoords] : newCoords, newMeasureState); - }); - - if (props.measurement.geomType === 'Polygon' && (!isEqual(newCoords[0], newCoords[newCoords.length - 1]))) { - newCoords = newCoords.concat([newCoords[0]]); - newMeasureState = set("feature.geometry.coordinates", [newCoords], newMeasureState); } + ); + if (updatedByUI) { + // update only re-calculated measures + this.props.updateMeasures(pick(newMeasureState, ["bearing", "area", "len", "point"])); + } else { + // update also the feature + this.props.changeMeasurementState(newMeasureState); } - this.invalidCoordinates = []; - this.props.changeMeasurementState(newMeasureState); }; - reprojectedCoordinates = (coordinates) => { + reprojectedCoordinatesIn4326 = (coordinates) => { return coordinates.map((coordinate) => { - let reprojectedCoordinate = reproject(coordinate, this.props.projection, 'EPSG:4326'); + let reprojectedCoordinate = reproject(coordinate, getProjectionCode(this.props.map), 'EPSG:4326'); return [reprojectedCoordinate.x, reprojectedCoordinate.y]; }); }; calculateGeodesicArea = (coordinates) => { - let reprojectedCoordinates = this.reprojectedCoordinates(coordinates); - return Math.abs(wgs84Sphere.geodesicArea(reprojectedCoordinates)); + if (coordinates.length >= 4 ) { + let reprojectedCoordinatesIn4326 = this.reprojectedCoordinatesIn4326(coordinates); + return Math.abs(wgs84Sphere.geodesicArea(reprojectedCoordinatesIn4326)); + } + return 0; }; /** @@ -486,10 +444,10 @@ class MeasurementSupport extends React.Component { const sketchCoords = line.getCoordinates(); if (props.measurement.geomType === 'Bearing' && sketchCoords.length > 1) { // calculate the azimuth as base for bearing information - const bearing = calculateAzimuth(sketchCoords[0], sketchCoords[1], props.projection); + const bearing = calculateAzimuth(sketchCoords[0], sketchCoords[1], getProjectionCode(props.map)); return getFormattedBearingValue(bearing); } - const reprojectedCoords = this.reprojectedCoordinates(sketchCoords); + const reprojectedCoords = this.reprojectedCoordinatesIn4326(sketchCoords); const length = calculateDistance(reprojectedCoords, props.measurement.lengthFormula); const {label, unit} = props.uom && props.uom.length; const output = round(convertUom(length, "m", unit), 2); diff --git a/web/client/components/map/openlayers/Overview.jsx b/web/client/components/map/openlayers/Overview.jsx index 087175ef76..6557ae6374 100644 --- a/web/client/components/map/openlayers/Overview.jsx +++ b/web/client/components/map/openlayers/Overview.jsx @@ -1,8 +1,16 @@ -var PropTypes = require('prop-types'); -var React = require('react'); -var ol = require('openlayers'); -var Layers = require('../../../utils/openlayers/Layers'); -var assign = require('object-assign'); +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const PropTypes = require('prop-types'); +const React = require('react'); +const ol = require('openlayers'); +const Layers = require('../../../utils/openlayers/Layers'); +const assign = require('object-assign'); +const {isFinite} = require('lodash'); require('./overview.css'); @@ -63,7 +71,7 @@ class Overview extends React.Component { return null; } - dragstart = (e) => { + dragstart = (event) => { if (!this.dragging) { this.dragBox = this.box.cloneNode(); this.dragBox.setAttribute("class", "ol-overview-dargbox"); @@ -79,18 +87,18 @@ class Overview extends React.Component { } else { this.offsetStartLeft = parseInt(this.box.style.left.slice(0, -2), 10); } - this.mouseStartTop = e.pageY; - this.mouseStartLeft = e.pageX; + this.mouseStartTop = event.pageY; + this.mouseStartLeft = event.pageX; this.dragging = true; } }; - draggingel = (e) => { + draggingel = (event) => { if (this.dragging === true) { - this.dragBox.style.top = this.offsetStartTop + e.pageY - this.mouseStartTop + 'px'; - this.dragBox.style.left = this.offsetStartLeft + e.pageX - this.mouseStartLeft + 'px'; - e.stopPropagation(); - e.preventDefault(); + this.dragBox.style.top = this.offsetStartTop + event.pageY - this.mouseStartTop + 'px'; + this.dragBox.style.left = this.offsetStartLeft + event.pageX - this.mouseStartLeft + 'px'; + event.stopPropagation(); + event.preventDefault(); } }; @@ -112,10 +120,12 @@ class Overview extends React.Component { let mapSize = this.props.map.getSize(); let xMove = offset.left * Math.abs(mapSize[0] / vWidth); let yMove = offset.top * Math.abs(mapSize[1] / vHeight); + xMove = isFinite(xMove) ? xMove : 0; + yMove = isFinite(yMove) ? yMove : 0; let bottomLeft = [0 + xMove, mapSize[1] + yMove]; let topRight = [mapSize[0] + xMove, 0 + yMove]; - let left = this.props.map.getCoordinateFromPixel(bottomLeft); - let top = this.props.map.getCoordinateFromPixel(topRight); + let left = this.props.map.getCoordinateFromPixel(bottomLeft) || [0, 0]; + let top = this.props.map.getCoordinateFromPixel(topRight) || [0, 0]; let extent = [left[0], left[1], top[0], top[1]]; this.props.map.getView().fit(extent, mapSize, {nearest: true}); }; diff --git a/web/client/components/map/openlayers/__tests__/DrawSupport-test.jsx b/web/client/components/map/openlayers/__tests__/DrawSupport-test.jsx index a78215f33c..15a414c3b1 100644 --- a/web/client/components/map/openlayers/__tests__/DrawSupport-test.jsx +++ b/web/client/components/map/openlayers/__tests__/DrawSupport-test.jsx @@ -12,443 +12,63 @@ const ol = require('openlayers'); const assign = require('object-assign'); const DrawSupport = require('../DrawSupport'); const {DEFAULT_ANNOTATIONS_STYLES} = require('../../../../utils/AnnotationsUtils'); +const {circle, geomCollFeature} = require('../../../../test-resources/drawsupport/features'); -let CIRCLE = { - type: 'FeatureCollection', - id: '36835090-23ad-11e8-9839-9bab136db9a3', - features: [{ - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ - [ - [ - -7.066692092065635, - 47.17477833929903 - ], - [ - -7.070957956986268, - 47.26697075050753 - ], - [ - -7.083738716328214, - 47.35863997502234 - ], - [ - -7.104983930273322, - 47.44942710493982 - ], - [ - -7.134609753668168, - 47.53897854842588 - ], - [ - -7.172499266922562, - 47.6269473500946 - ], - [ - -7.218502937437762, - 47.71299445745591 - ], - [ - -7.272439209743323, - 47.796789930095706 - ], - [ - -7.334095222013593, - 47.87801408888858 - ], - [ - -7.403227646136091, - 47.95635860315586 - ], - [ - -7.479563648016411, - 48.03152751426181 - ], - [ - -7.562801964329729, - 48.10323819468144 - ], - [ - -7.6526140914695295, - 48.17122224206827 - ], - [ - -7.748645582001249, - 48.235226308292674 - ], - [ - -7.850517443504369, - 48.29501286380931 - ], - [ - -7.95782763428237, - 48.35036089804014 - ], - [ - -8.07015265003761, - 48.40106655672712 - ], - [ - -8.18704919524932, - 48.44694371741463 - ], - [ - -8.308055932658505, - 48.48782450436609 - ], - [ - -8.432695303955327, - 48.52355974430404 - ], - [ - -8.560475414483594, - 48.55401936438973 - ], - [ - -8.690891974524225, - 48.57909273383168 - ], - [ - -8.823430289496404, - 48.59868895043517 - ], - [ - -8.95756729122193, - 48.612737073283476 - ], - [ - -9.092773602236358, - 48.62118630258009 - ], - [ - -9.228515625000002, - 48.62400610748772 - ], - [ - -9.364257647763646, - 48.62118630258009 - ], - [ - -9.499463958778072, - 48.612737073283476 - ], - [ - -9.633600960503598, - 48.59868895043517 - ], - [ - -9.766139275475776, - 48.57909273383168 - ], - [ - -9.89655583551641, - 48.55401936438973 - ], - [ - -10.024335946044676, - 48.52355974430404 - ], - [ - -10.148975317341497, - 48.48782450436609 - ], - [ - -10.269982054750683, - 48.44694371741463 - ], - [ - -10.386878599962396, - 48.40106655672712 - ], - [ - -10.499203615717633, - 48.35036089804014 - ], - [ - -10.606513806495634, - 48.29501286380931 - ], - [ - -10.708385667998753, - 48.235226308292674 - ], - [ - -10.804417158530473, - 48.17122224206827 - ], - [ - -10.894229285670272, - 48.10323819468144 - ], - [ - -10.977467601983593, - 48.03152751426181 - ], - [ - -11.053803603863912, - 47.95635860315586 - ], - [ - -11.12293602798641, - 47.87801408888858 - ], - [ - -11.184592040256682, - 47.796789930095706 - ], - [ - -11.238528312562241, - 47.71299445745591 - ], - [ - -11.284531983077443, - 47.6269473500946 - ], - [ - -11.322421496331836, - 47.53897854842588 - ], - [ - -11.352047319726681, - 47.44942710493982 - ], - [ - -11.373292533671789, - 47.35863997502234 - ], - [ - -11.386073293013734, - 47.26697075050753 - ], - [ - -11.390339157934367, - 47.17477833929903 - ], - [ - -11.386073293013734, - 47.08242559504841 - ], - [ - -11.373292533671789, - 46.990277901535556 - ], - [ - -11.35204731972668, - 46.8987017170481 - ], - [ - -11.322421496331836, - 46.808063084694076 - ], - [ - -11.284531983077443, - 46.718726115193576 - ], - [ - -11.238528312562241, - 46.63105144927073 - ], - [ - -11.184592040256682, - 46.545394707297326 - ], - [ - -11.12293602798641, - 46.46210493431348 - ], - [ - -11.053803603863912, - 46.381523048960865 - ], - [ - -10.977467601983593, - 46.303980305201314 - ], - [ - -10.894229285670272, - 46.22979677595146 - ], - [ - -10.804417158530473, - 46.15927986793656 - ], - [ - -10.708385667998753, - 46.09272287714786 - ], - [ - -10.60651380649563, - 46.03040359427646 - ], - [ - -10.499203615717633, - 45.972582969388 - ], - [ - -10.386878599962396, - 45.919503844897314 - ], - [ - -10.269982054750683, - 45.871389765601215 - ], - [ - -10.148975317341497, - 45.828443874131516 - ], - [ - -10.024335946044676, - 45.79084789970411 - ], - [ - -9.89655583551641, - 45.75876124746622 - ], - [ - -9.766139275475776, - 45.73232019509079 - ], - [ - -9.633600960503598, - 45.711637202538626 - ], - [ - -9.499463958778076, - 45.696800340115416 - ], - [ - -9.364257647763646, - 45.6878728390993 - ], - [ - -9.228515625000002, - 45.684892768315834 - ], - [ - -9.092773602236358, - 45.6878728390993 - ], - [ - -8.95756729122193, - 45.696800340115416 - ], - [ - -8.823430289496406, - 45.711637202538626 - ], - [ - -8.690891974524225, - 45.73232019509079 - ], - [ - -8.560475414483594, - 45.75876124746622 - ], - [ - -8.43269530395533, - 45.79084789970411 - ], - [ - -8.308055932658508, - 45.828443874131516 - ], - [ - -8.187049195249319, - 45.871389765601215 - ], - [ - -8.07015265003761, - 45.919503844897314 - ], - [ - -7.95782763428237, - 45.972582969388 - ], - [ - -7.850517443504371, - 46.03040359427646 - ], - [ - -7.7486455820012505, - 46.09272287714786 - ], - [ - -7.652614091469531, - 46.15927986793656 - ], - [ - -7.562801964329729, - 46.22979677595146 - ], - [ - -7.479563648016411, - 46.3039803052013 - ], - [ - -7.403227646136092, - 46.381523048960865 - ], - [ - -7.334095222013593, - 46.46210493431348 - ], - [ - -7.272439209743323, - 46.545394707297326 - ], - [ - -7.218502937437762, - 46.63105144927073 - ], - [ - -7.172499266922562, - 46.718726115193576 - ], - [ - -7.134609753668168, - 46.808063084694076 - ], - [ - -7.104983930273322, - 46.8987017170481 - ], - [ - -7.083738716328214, - 46.990277901535556 - ], - [ - -7.070957956986268, - 47.08242559504841 - ], - [ - -7.066692092065635, - 47.17477833929903 - ] - ] - ] - } - }], - newFeature: true, - properties: { - id: '36835090-23ad-11e8-9839-9bab136db9a3', - circles: [0], - isCircle: true - }, - style: { - type: 'Circle', - Circle: { - color: '#ffcc33', - opacity: 1, - weight: 3, - fillColor: '#ffffff', - fillOpacity: 0.2, - radius: 10 - } - } +const viewOptions = { + projection: 'EPSG:3857', + center: [0, 0], + zoom: 5 +}; +let olMap = new ol.Map({ + target: "map", + view: new ol.View(viewOptions) +}); +olMap.disableEventListener = () => {}; + +const testHandlers = { + onStatusChange: () => {}, + onSelectFeatures: () => {}, + onGeometryChanged: () => {}, + onEndDrawing: () => {}, + onDrawingFeatures: () => {} +}; + +/* used to render the DrawSupport component with some default props*/ +const renderDrawSupport = (props = {}) => { + return ReactDOM.render( + , document.getElementById("container")); }; +/** + * it renders Drawsupport in edit mode with singleclick Listener enabled and + * it dispatches a singleclick mouse event +*/ +const renderAndClick = (props = {}, options = {}) => { + let support = renderDrawSupport(); + // entering componentWillReceiveProps + support = renderDrawSupport({ + drawStatus: "drawOrEdit", + features: [props.feature], + options: { + drawEnabled: false, + editEnabled: true, + addClickCallback: true + }, + ...props + }); + support.props.map.dispatchEvent({ + type: "singleclick", + coordinate: options.singleClickCoordiante || [500, 30] + }); + return support; +}; + + describe('Test DrawSupport', () => { beforeEach((done) => { document.body.innerHTML = '
'; @@ -457,6 +77,13 @@ describe('Test DrawSupport', () => { afterEach((done) => { document.body.innerHTML = ''; + olMap = new ol.Map({ + target: "map", + view: new ol.View(viewOptions) + }); + olMap.disableEventListener = () => {}; + + expect.restoreSpies(); setTimeout(done); }); @@ -489,19 +116,24 @@ describe('Test DrawSupport', () => { getView: () => ({getProjection: () => ({getCode: () => "EPSG:3857"})}), disableEventListener: () => {}, addInteraction: () => {}, + enableEventListener: () => {}, + removeInteraction: () => {}, + removeLayer: () => {}, getInteractions: () => ({ getLength: () => 0 }) }; - const spyAdd = expect.spyOn(fakeMap, "addLayer"); - const spyInteraction = expect.spyOn(fakeMap, "addInteraction"); + const spyAddLayer = expect.spyOn(fakeMap, "addLayer"); + const spyAddInteraction = expect.spyOn(fakeMap, "addInteraction"); const support = ReactDOM.render( , document.getElementById("container")); expect(support).toExist(); ReactDOM.render( , document.getElementById("container")); - expect(spyAdd.calls.length).toBe(1); - expect(spyInteraction.calls.length).toBe(1); + ReactDOM.render( + , document.getElementById("container")); + expect(spyAddLayer.calls.length).toBe(2); + expect(spyAddInteraction.calls.length).toBe(2); }); it('starts drawing bbox', () => { @@ -579,9 +211,6 @@ describe('Test DrawSupport', () => { }) }; - const testHandlers = { - onStatusChange: () => {} - }; const ft = { type: "Feature", geometry: { @@ -619,9 +248,6 @@ describe('Test DrawSupport', () => { }) }; - const testHandlers = { - onStatusChange: () => {} - }; const spyChangeStatus = expect.spyOn(testHandlers, "onStatusChange"); const support = ReactDOM.render( @@ -746,10 +372,6 @@ describe('Test DrawSupport', () => { }) }) }; - const testHandlers = { - onEndDrawing: () => {}, - onStatusChange: () => {} - }; const feature = new ol.Feature({ geometry: new ol.geom.Point(13.0, 43.0), name: 'My Point' @@ -762,6 +384,10 @@ describe('Test DrawSupport', () => { ReactDOM.render( , document.getElementById("container")); + support.drawInteraction.dispatchEvent({ + type: 'drawstart', + feature: feature + }); support.drawInteraction.dispatchEvent({ type: 'drawend', feature: feature @@ -770,7 +396,7 @@ describe('Test DrawSupport', () => { expect(spyChangeStatus.calls.length).toBe(1); }); - it('end drawing with continue', () => { + it('end drawing a circle feature ', () => { const fakeMap = { addLayer: () => {}, disableEventListener: () => {}, @@ -784,9 +410,46 @@ describe('Test DrawSupport', () => { }) }) }; - const testHandlers = { - onEndDrawing: () => {}, - onStatusChange: () => {} + const feature = new ol.Feature({ + geometry: new ol.geom.Circle([13.0, 43.0], 100), + name: 'My Point' + }); + const spyEnd = expect.spyOn(testHandlers, "onEndDrawing"); + const spyChangeStatus = expect.spyOn(testHandlers, "onStatusChange"); + const support = ReactDOM.render( + , document.getElementById("container")); + expect(support).toExist(); + ReactDOM.render( + , document.getElementById("container")); + support.drawInteraction.dispatchEvent({ + type: 'drawstart', + feature: feature + }); + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: feature + }); + expect(spyEnd.calls.length).toBe(1); + expect(spyChangeStatus.calls.length).toBe(1); + }); + + it('end drawing with continue', () => { + const fakeMap = { + addLayer: () => {}, + disableEventListener: () => {}, + addInteraction: () => {}, + getInteractions: () => ({ + getLength: () => 0 + }), + getView: () => ({ + getProjection: () => ({ + getCode: () => 'EPSG:4326' + }) + }) }; const feature = new ol.Feature({ geometry: new ol.geom.Point(13.0, 43.0), @@ -1478,11 +1141,6 @@ describe('Test DrawSupport', () => { }) }; - const testHandlers = { - onEndDrawing: () => {}, - onStatusChange: () => {}, - onGeometryChanged: () => {} - }; const geoJSON = { type: 'Feature', geometry: { @@ -1537,11 +1195,6 @@ describe('Test DrawSupport', () => { }) }; - const testHandlers = { - onEndDrawing: () => {}, - onStatusChange: () => {}, - onGeometryChanged: () => {} - }; const geoJSON = { type: 'Feature', geometry: { @@ -1599,7 +1252,7 @@ describe('Test DrawSupport', () => { }; const support = ReactDOM.render( - , document.getElementById("container")); + , document.getElementById("container")); expect(support).toExist(); const center = [1, 1]; const radius = 100; @@ -1931,4 +1584,501 @@ describe('Test DrawSupport', () => { }, 100); }); + it('test draw callbacks in edit mode with Polygons feature', (done) => { + const feature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [13, 43], + [15, 43], + [15, 44], + [13, 43] + ]] + }, + properties: { + name: "some name", + id: "a-unique-id", + canEdit: true + }, + style: [{ + id: "style-id", + color: "#FF0000", + opacity: 1, + fillColor: "#0000FF", + fillOpacity: 1 + }] + }; + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderAndClick({ + feature, + drawMethod: feature.geometry.type + }); + expect(support).toExist(); + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + const ft = spyOnDrawingFeatures.calls[0].arguments[0][0]; + expect(ft.type).toBe("Feature"); + expect(ft.geometry.type).toBe("Polygon"); + expect(ft.properties).toEqual({ + "name": "some name", + "id": "a-unique-id", + "canEdit": true + }); + done(); + }); + it('test draw callbacks in edit mode with LineString feature', (done) => { + const feature = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [ + [13, 43], + [15, 43], + [15, 44], + [13, 43] + ] + }, + properties: { + name: "some name", + id: "a-unique-id", + canEdit: true + }, + style: [{ + id: "style-id", + color: "#FF0000", + opacity: 1 + }] + }; + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderAndClick({ + feature, + drawMethod: feature.geometry.type + }); + expect(support).toExist(); + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + done(); + }); + + it('test draw callbacks in edit mode with Text feature', (done) => { + const feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [13, 43] + }, + properties: { + name: "some name", + id: "a-unique-id", + valueText: "a text", + canEdit: true, + isText: true + }, + style: [{ + id: "style-id", + color: "#FF0000", + label: "a text", + opacity: 1 + }] + }; + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderAndClick({ + feature, + drawMethod: "Text" + }); + expect(support).toExist(); + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + done(); + }); + + it('test draw callbacks in edit mode with Circle feature', (done) => { + const feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [13, 43] + }, + properties: { + name: "some name", + id: "a-unique-id", + valueText: "a text", + canEdit: true, + radius: 1111, + isCircle: true + }, + style: [{ + id: "style-id", + color: "#FF0000", + opacity: 1 + }] + }; + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderAndClick({ + feature, + drawMethod: "Circle" + }); + expect(support).toExist(); + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + done(); + }); + it('test drawend callbacks with Circle, transformed int feature collection', (done) => { + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + features: [null], + drawMethod: "Circle", + drawStatus: "drawOrEdit", + options: { + transformToFeatureCollection: true, + drawEnabled: true + } + }); + expect(support).toExist(); + const center = [1300, 4300]; + const radius = 1000; + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.Circle(center, radius) + }) + }); + const drawOwner = null; + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + expect(ArgsGeometryChanged[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[2]).toEqual(""); + expect(ArgsGeometryChanged[3]).toEqual(false); + expect(ArgsGeometryChanged[4]).toEqual(true); + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[0][0]).toEqual(ArgsEndDrawing[0]); + + done(); + }); + + it('test drawend callbacks with Text, transformed int feature collection', (done) => { + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + features: [null], + drawMethod: "Text", + drawStatus: "drawOrEdit", + options: { + transformToFeatureCollection: true, + stopAfterDrawing: true, + drawEnabled: true + } + }); + expect(support).toExist(); + const coordinate = [1300, 4300]; + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.Point(coordinate) + }) + }); + const drawOwner = null; + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + expect(ArgsGeometryChanged[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[2]).toEqual("enterEditMode"); + expect(ArgsGeometryChanged[3]).toEqual(true); + expect(ArgsGeometryChanged[4]).toEqual(false); + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[0][0]).toEqual(ArgsEndDrawing[0]); + expect(ArgsEndDrawing[0].features[0].properties.isText).toBe(true); + expect(ArgsEndDrawing[0].features[0].properties.valueText).toBe("."); + done(); + }); + it('test drawend callbacks with Polygon, transformed int feature collection', (done) => { + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + features: [null], + drawMethod: "Polygon", + drawStatus: "drawOrEdit", + options: { + transformToFeatureCollection: true, + stopAfterDrawing: true, + drawEnabled: true + } + }); + expect(support).toExist(); + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.Polygon([[[1300, 4300], [8, 9], [8, 59]]]) + }) + }); + const drawOwner = null; + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + expect(ArgsGeometryChanged[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[2]).toEqual("enterEditMode"); + expect(ArgsGeometryChanged[3]).toEqual(false); + expect(ArgsGeometryChanged[4]).toEqual(false); + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[0][0]).toEqual(ArgsEndDrawing[0]); + expect(ArgsEndDrawing[0].features[0].geometry.coordinates[0].length).toBe(4); + done(); + }); + + it('test drawend callbacks with LineString, transformed int feature collection', (done) => { + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + features: [null], + drawMethod: "LineString", + drawStatus: "drawOrEdit", + options: { + transformToFeatureCollection: true, + drawEnabled: true + } + }); + expect(support).toExist(); + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.LineString([[1300, 4300], [8, 9], [8, 59]]) + }) + }); + const drawOwner = null; + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + expect(ArgsGeometryChanged[3]).toEqual(false); + expect(ArgsGeometryChanged[4]).toEqual(false); + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[1]).toBe(drawOwner); + expect(ArgsGeometryChanged[0][0]).toEqual(ArgsEndDrawing[0]); + expect(ArgsEndDrawing[0].features[0].geometry.coordinates.length).toBe(3); + done(); + }); + + it('test drawend callbacks with Circle, exported as geomColl', (done) => { + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "Circle", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + transformToFeatureCollection: false, + drawEnabled: true + } + }); + expect(support).toExist(); + const center = [1300, 4300]; + const radius = 1000; + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.Circle(center, radius) + }) + }); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[1]).toBe(null); + expect(ArgsEndDrawing[0].geometry.type).toBe("GeometryCollection"); + expect(ArgsEndDrawing[0].geometry.geometries.length).toBe(2); + + done(); + }); + + it('test drawend callbacks with MultiLineString, exported as geomColl', (done) => { + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "MultiLineString", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + transformToFeatureCollection: false, + drawEnabled: true + } + }); + expect(support).toExist(); + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.MultiLineString([[[1300, 4300], [8, 9], [8, 59]]]) + }) + }); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[0].geometry.type).toBe("GeometryCollection"); + expect(ArgsEndDrawing[0].geometry.geometries.length).toBe(2); + + done(); + }); + + it('test drawend callbacks with MultiPoint, exported as geomColl', (done) => { + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnEndDrawing = expect.spyOn(testHandlers, "onEndDrawing"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "MultiPoint", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + transformToFeatureCollection: false, + drawEnabled: true + } + }); + expect(support).toExist(); + support.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.Point([1300, 4300]) + }) + }); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnGeometryChanged.calls.length).toBe(1); + const ArgsGeometryChanged = spyOnGeometryChanged.calls[0].arguments; + expect(ArgsGeometryChanged.length).toBe(5); + + expect(spyOnEndDrawing).toHaveBeenCalled(); + expect(spyOnEndDrawing.calls.length).toBe(1); + const ArgsEndDrawing = spyOnEndDrawing.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(2); + expect(ArgsEndDrawing[0].geometry.type).toBe("GeometryCollection"); + expect(ArgsEndDrawing[0].geometry.geometries.length).toBe(2); + + done(); + }); + it('test select interaction, retrieving a drawn feature', (done) => { + const spyOnSelectFeatures = expect.spyOn(testHandlers, "onSelectFeatures"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "LineString", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + selected: geomCollFeature, + transformToFeatureCollection: false, + selectEnabled: true + } + }); + expect(support).toExist(); + const feature = new ol.Feature({ + geometry: new ol.geom.Point(13.0, 43.0), + name: 'My Point' + }); + support.selectInteraction.dispatchEvent({ + type: 'select', + feature: feature + }); + expect(spyOnSelectFeatures).toHaveBeenCalled(); + done(); + }); + it('test modifyend event for modifyInteraction with Circle, exported FeatureCollection', (done) => { + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "Circle", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + transformToFeatureCollection: true, + editEnabled: true + } + }); + expect(support).toExist(); + const center = [1300, 4300]; + const radius = 1000; + support.modifyInteraction.dispatchEvent({ + type: 'modifyend', + features: new ol.Collection( + [new ol.Feature({ + geometry: new ol.geom.Circle(center, radius) + })] + ) + }); + expect(spyOnGeometryChanged).toNotHaveBeenCalled(); + expect(spyOnDrawingFeatures).toHaveBeenCalled(); + expect(spyOnDrawingFeatures.calls.length).toBe(1); + const ArgsEndDrawing = spyOnDrawingFeatures.calls[0].arguments; + expect(ArgsEndDrawing.length).toBe(1); + + done(); + }); + + it('test modifyend event for modifyInteraction with Circle, exported FeatureCollection', (done) => { + const spyOnGeometryChanged = expect.spyOn(testHandlers, "onGeometryChanged"); + const spyOnDrawingFeatures = expect.spyOn(testHandlers, "onDrawingFeatures"); + let support = renderDrawSupport(); + support = renderDrawSupport({ + drawMethod: "Circle", + drawStatus: "drawOrEdit", + features: [geomCollFeature], + options: { + transformToFeatureCollection: false, + editEnabled: true + } + }); + expect(support).toExist(); + const center = [1300, 4300]; + const radius = 1000; + support.modifyInteraction.dispatchEvent({ + type: 'modifyend', + features: new ol.Collection( + [new ol.Feature({ + geometry: new ol.geom.Circle(center, radius) + })] + ) + }); + expect(spyOnGeometryChanged).toHaveBeenCalled(); + expect(spyOnDrawingFeatures).toNotHaveBeenCalled(); + + done(); + }); }); diff --git a/web/client/components/map/openlayers/__tests__/LegacyVectorStyle-test.js b/web/client/components/map/openlayers/__tests__/LegacyVectorStyle-test.js index 852e04d959..06c5175a89 100644 --- a/web/client/components/map/openlayers/__tests__/LegacyVectorStyle-test.js +++ b/web/client/components/map/openlayers/__tests__/LegacyVectorStyle-test.js @@ -8,6 +8,8 @@ const expect = require('expect'); const LegacyVectorStyle = require('../LegacyVectorStyle'); const ol = require('openlayers'); +const {geomCollFeature} = require('../../../../test-resources/drawsupport/features'); +const {DEFAULT_ANNOTATIONS_STYLES} = require('../../../../utils/AnnotationsUtils'); describe('Test LegacyVectorStyle', () => { beforeEach((done) => { @@ -388,4 +390,26 @@ describe('Test LegacyVectorStyle', () => { expect(olStroke.getWidth()).toBe(10); expect(olStroke.getLineDash()).toEqual(['10', '5']); }); + + it('test getStyle with GeometryCollection', () => { + const styleFunc = LegacyVectorStyle.getStyle({ + features: [geomCollFeature], + style: { + color: "ff0000", + opacity: 0.5, + ...DEFAULT_ANNOTATIONS_STYLES + } + }, false, ["textValue"]); + expect(styleFunc).toExist(); + + const styleGenerated = styleFunc(new ol.Feature({ + geometry: new ol.geom.GeometryCollection([ + new ol.geom.LineString([[1, 2], [1, 3]]), + new ol.geom.Polygon([[1, 2], [1, 3], [1, 1], [1, 2]]), + new ol.geom.Point([1, 20]) + ]) + })); + expect(styleGenerated).toExist(); + }); + }); diff --git a/web/client/components/map/openlayers/__tests__/MeasurementSupport-test.jsx b/web/client/components/map/openlayers/__tests__/MeasurementSupport-test.jsx index 8bc0978aaf..08fef654f8 100644 --- a/web/client/components/map/openlayers/__tests__/MeasurementSupport-test.jsx +++ b/web/client/components/map/openlayers/__tests__/MeasurementSupport-test.jsx @@ -6,17 +6,72 @@ * LICENSE file in the root directory of this source tree. */ -var expect = require('expect'); -var React = require('react'); -var ReactDOM = require('react-dom'); -var ol = require('openlayers'); -var MeasurementSupport = require('../MeasurementSupport'); +const expect = require('expect'); +const React = require('react'); +const ReactDOM = require('react-dom'); +const ol = require('openlayers'); +const {round} = require('lodash'); +const MeasurementSupport = require('../MeasurementSupport'); +const { + lineFeature, + lineFeature3, + polyFeatureClosed +} = require('../../../../test-resources/drawsupport/features'); describe('Openlayers MeasurementSupport', () => { - var msNode; - function getMapLayersNum(map) { - return map.getLayers().getLength(); + let msNode; + + /* basic objects */ + const viewOptions = { + projection: 'EPSG:3857', + center: [0, 0], + zoom: 5 + }; + let map = new ol.Map({ + target: "map", + view: new ol.View(viewOptions) + }); + const uom = { + length: {unit: 'm', label: 'm'}, + area: {unit: 'sqm', label: 'm²'} + }; + + const testHandlers = { + changeMeasurementState: () => {}, + updateMeasures: () => {}, + changeGeometry: () => {} + }; + function getMapLayersNum(olMap) { + return olMap.getLayers().getLength(); } + /* utility used to render the MeasurementSupport component with some default props*/ + const renderMeasurement = (props = {}) => { + return ReactDOM.render( + , msNode); + }; + + /** + * it renders the measure support with draw interaction enabled + */ + const renderWithDrawing = (props = {}) => { + let cmp = renderMeasurement(); + // entering componentWillReceiveProps + cmp = renderMeasurement({ + measurement: { + feature: {}, + geomType: "LineString" + }, + ...props + }); + return cmp; + }; beforeEach((done) => { document.body.innerHTML = '
'; @@ -27,104 +82,201 @@ describe('Openlayers MeasurementSupport', () => { ReactDOM.unmountComponentAtNode(msNode); document.body.innerHTML = ''; msNode = undefined; - setTimeout(done); - }); - - it('test creation', () => { - var viewOptions = { - projection: 'EPSG:3857', - center: [0, 0], - zoom: 5 - }; - var map = new ol.Map({ + expect.restoreSpies(); + map = new ol.Map({ target: "map", view: new ol.View(viewOptions) }); + setTimeout(done); + }); - const cmp = ReactDOM.render( - - , msNode); - + it('test creation', () => { + const cmp = renderMeasurement(); expect(cmp).toExist(); }); - it('test if a new layer is added to the map in order to allow drawing.', () => { - var viewOptions = { - projection: 'EPSG:3857', - center: [0, 0], - zoom: 5 - }; - var map = new ol.Map({ - target: "map", - view: new ol.View(viewOptions) - }); - - let cmp = ReactDOM.render( - - , msNode); + let cmp = renderMeasurement(); expect(cmp).toExist(); let initialLayersNum = getMapLayersNum(map); - cmp = ReactDOM.render( - - , msNode); + cmp = renderMeasurement({ + measurement: { + geomType: "LineString", + showLabel: true + } + }); expect(getMapLayersNum(map)).toBeGreaterThan(initialLayersNum); }); - it('test if drawing layers will be removed', () => { - var viewOptions = { - projection: 'EPSG:3857', - center: [0, 0], - zoom: 5 - }; - var map = new ol.Map({ - target: "map", - view: new ol.View(viewOptions) - }); - - let cmp = ReactDOM.render( - - , msNode); + let cmp = renderMeasurement(); expect(cmp).toExist(); let initialLayersNum = getMapLayersNum(map); - cmp = ReactDOM.render( - - , msNode); + cmp = renderMeasurement({ + measurement: { + geomType: "Polygon" + } + }); + expect(getMapLayersNum(map)).toBeGreaterThan(initialLayersNum); - cmp = ReactDOM.render( - - , msNode); + cmp = renderMeasurement(); expect(getMapLayersNum(map)).toBe(initialLayersNum); }); + it('test updating distance (LineString) tooltip after change uom', () => { + const spyOnChangeMeasurementState = expect.spyOn(testHandlers, "changeMeasurementState"); + const spyUpdateMeasures = expect.spyOn(testHandlers, "updateMeasures"); + let cmp = renderWithDrawing(); + expect(cmp).toExist(); + cmp = renderMeasurement({ + measurement: { + geomType: "LineString", + feature: lineFeature, + lineMeasureEnabled: true, + updatedByUI: true, + showLabel: true + }, + uom + }); + expect(spyOnChangeMeasurementState).toNotHaveBeenCalled(); + expect(spyUpdateMeasures).toHaveBeenCalled(); + expect(spyUpdateMeasures.calls.length).toBe(1); + const measureState = spyUpdateMeasures.calls[0].arguments[0]; + expect(measureState).toExist(); + expect(round(measureState.len, 2)).toBe(400787.44); + expect(measureState.bearing).toBe(0); + expect(measureState.area).toBe(0); + expect(measureState.point).toBe(null); + }); + it('test updating Bearing (LineString) tooltip after change uom', () => { + const spyUpdateMeasures = expect.spyOn(testHandlers, "updateMeasures"); + const spyOnChangeMeasurementState = expect.spyOn(testHandlers, "changeMeasurementState"); + let cmp = renderWithDrawing(); + expect(cmp).toExist(); + cmp = renderMeasurement({ + measurement: { + geomType: "Bearing", + feature: lineFeature, + bearingMeasureEnabled: true, + updatedByUI: true, + showLabel: true + }, + uom + }); + expect(spyOnChangeMeasurementState).toNotHaveBeenCalled(); + expect(spyUpdateMeasures).toHaveBeenCalled(); + expect(spyUpdateMeasures.calls.length).toBe(1); + const measureState = spyUpdateMeasures.calls[0].arguments[0]; + expect(measureState).toExist(); + expect(measureState.len).toBe(0); + expect(round(measureState.bearing, 2)).toBe(33.63); + expect(measureState.area).toBe(0); + expect(measureState.point).toBe(null); + }); + it('test updating area (Polygon) tooltip after change uom', () => { + const spyUpdateMeasures = expect.spyOn(testHandlers, "updateMeasures"); + const spyOnChangeMeasurementState = expect.spyOn(testHandlers, "changeMeasurementState"); + let cmp = renderWithDrawing(); + expect(cmp).toExist(); + cmp = renderMeasurement({ + measurement: { + geomType: "Polygon", + feature: polyFeatureClosed, + areaMeasureEnabled: true, + updatedByUI: true, + showLabel: false + }, + uom + }); + expect(spyOnChangeMeasurementState).toNotHaveBeenCalled(); + expect(spyUpdateMeasures).toHaveBeenCalled(); + expect(spyUpdateMeasures.calls.length).toBe(1); + const measureState = spyUpdateMeasures.calls[0].arguments[0]; + expect(measureState).toExist(); + expect(round(measureState.area, 2)).toBe(49490132941.51); + expect(measureState.bearing).toBe(0); + }); + + it('test drawInteraction callbacks for a distance (LineString)', () => { + const spyOnChangeMeasurementState = expect.spyOn(testHandlers, "changeMeasurementState"); + const spyUpdateMeasures = expect.spyOn(testHandlers, "updateMeasures"); + const spyOnChangeGeometry = expect.spyOn(testHandlers, "changeGeometry"); + let cmp = renderWithDrawing(); + expect(cmp).toExist(); + cmp = renderMeasurement({ + measurement: { + geomType: "LineString", + feature: lineFeature, + lineMeasureEnabled: true, + updatedByUI: false, + showLabel: true + }, + uom + }); + cmp.drawInteraction.dispatchEvent({ + type: 'drawstart', + feature: new ol.Feature({ + geometry: new ol.geom.LineString([[13.0, 43.0], [13.0, 40.0]]), + name: 'My line with 2 points' + }) + }); + cmp.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.LineString([[13.0, 43.0], [13.0, 40.0], [11.0, 41.0]]), + name: 'My line with 3 points' + }) + }); + expect(spyOnChangeMeasurementState).toNotHaveBeenCalled(); + expect(spyUpdateMeasures).toNotHaveBeenCalled(); + expect(spyOnChangeGeometry).toHaveBeenCalled(); + const changedFeature = spyOnChangeGeometry.calls[0].arguments[0]; + expect(changedFeature.type).toBe("Feature"); + expect(changedFeature.geometry.coordinates.length).toBe(3); + + }); + it('test drawing a distance (LineString) and moving pointer', () => { + let cmp = renderWithDrawing(); + expect(cmp).toExist(); + cmp = renderMeasurement({ + measurement: { + geomType: "LineString", + feature: lineFeature, + lineMeasureEnabled: true, + updatedByUI: true, + showLabel: true + }, + uom + }); + cmp.drawInteraction.dispatchEvent({ + type: 'drawstart', + feature: new ol.Feature({ + geometry: new ol.geom.LineString([[13.0, 43.0], [13.0, 40.0]]), + name: 'My line with 2 points' + }) + }); + cmp.drawInteraction.dispatchEvent({ + type: 'drawend', + feature: new ol.Feature({ + geometry: new ol.geom.LineString([[13.0, 43.0], [13.0, 40.0], [11.0, 41.0]]), + name: 'My line with 3 points' + }) + }); + cmp = renderMeasurement({ + measurement: { + geomType: "LineString", + feature: lineFeature3, + lineMeasureEnabled: true, + updatedByUI: true, + showLabel: true + }, + uom + }); + map.dispatchEvent({ + type: 'pointermove', + coordinate: [100, 400] + }); + expect(cmp.helpTooltip.getPosition()).toEqual([100, 400]); + + }); + }); diff --git a/web/client/components/map/openlayers/__tests__/Overview-test.jsx b/web/client/components/map/openlayers/__tests__/Overview-test.jsx index c723461ccc..949c75777c 100644 --- a/web/client/components/map/openlayers/__tests__/Overview-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Overview-test.jsx @@ -5,11 +5,11 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -var expect = require('expect'); -var React = require('react'); -var ReactDOM = require('react-dom'); -var ol = require('openlayers'); -var Overview = require('../Overview'); +const expect = require('expect'); +const React = require('react'); +const ReactDOM = require('react-dom'); +const ol = require('openlayers'); +const Overview = require('../Overview'); describe('Openlayers Overview component', () => { @@ -46,4 +46,28 @@ describe('Openlayers Overview component', () => { const overview = domMap.getElementsByClassName('ol-overviewmap'); expect(overview.length).toBe(1); }); + + it('testing mouse events', () => { + const ov = ReactDOM.render(, document.getElementById("container")); + expect(ov).toExist(); + const domMap = map.getViewport(); + const overview = domMap.getElementsByClassName('ol-overviewmap'); + expect(overview.length).toBe(1); + ov.box.onmousedown({ + pageX: 1, + pageY: 1 + }); + ov.box.onmousemove({ + pageX: 3, + pageY: 3, + stopPropagation: () => {}, + preventDefault: () => {} + }); + ov.box.onmouseup({ + pageX: 3, + pageY: 3 + }); + expect(ov.box.onmouseup).toBe(null); + expect(ov.box.onmousemove).toBe(null); + }); }); diff --git a/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx b/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx index 8514b5e200..5a314b4f75 100644 --- a/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx +++ b/web/client/components/mapcontrols/annotations/CoordinatesEditor.jsx @@ -70,9 +70,9 @@ class CoordinateEditor extends React.Component { "Polygon": {min: 3, add: true, remove: true, validation: "validateCoordinates", notValid: "annotations.editor.notValidPolyline"}, "LineString": {min: 2, add: true, remove: true, validation: "validateCoordinates", notValid: "annotations.editor.notValidPolyline"}, "MultiPoint": {min: 2, add: true, remove: true, validation: "validateCoordinates", notValid: "annotations.editor.notValidPolyline"}, - "Point": {min: 1, add: false, remove: false, validation: "validateCoordinates", notValid: "annotations.editor.notValidMarker"}, - "Circle": {add: false, remove: false, validation: "validateCircle", notValid: "annotations.editor.notValidCircle"}, - "Text": {add: false, remove: false, validation: "validateText", notValid: "annotations.editor.notValidText"} + "Point": {min: 1, max: 1, add: true, remove: false, validation: "validateCoordinates", notValid: "annotations.editor.notValidMarker"}, + "Circle": {min: 1, max: 1, add: true, remove: false, validation: "validateCircle", notValid: "annotations.editor.notValidCircle"}, + "Text": {min: 1, max: 1, add: true, remove: false, validation: "validateText", notValid: "annotations.editor.notValidText"} }, transitionProps: { transitionName: "switch-panel-transition", @@ -191,6 +191,9 @@ class CoordinateEditor extends React.Component { onClick: () => { let tempComps = [...this.props.components]; tempComps = tempComps.concat([{lat: "", lon: ""}]); + if (this.props.type === "Polygon") { + tempComps = this.addCoordPolygon(tempComps); + } this.props.onChange(tempComps, this.props.properties.radius, this.props.properties.valueText); } } @@ -211,13 +214,13 @@ class CoordinateEditor extends React.Component { {this.props.type === "Circle" && this.renderCircle()} {this.props.type === "Text" && this.renderText()} - - - { - this.props.type === "Circle" && Center - } - - + { + this.props.type === "Circle" && + + + + + } {!(!this.props.components || this.props.components.length === 0) && @@ -228,7 +231,7 @@ class CoordinateEditor extends React.Component { } - + {this.props.components.map((component, idx) => { if (this.props.isMouseEnterEnabled || this.props.type === "LineString" || this.props.type === "Polygon" || this.props.type === "MultiPoint") { @@ -294,12 +297,14 @@ class CoordinateEditor extends React.Component { ); } - validateCoordinates = (components = this.props.components, remove = false) => { + validateCoordinates = (components = this.props.components, remove = false, idx) => { if (components && components.length) { const validComponents = components.filter(validateCoords); if (remove) { - return validComponents.length > this.props.componentsValidation[this.props.type].min; + return validComponents.length > this.props.componentsValidation[this.props.type].min || + // if there are at least the min number of valid points, then you can delete the other invalid ones + validComponents.length >= this.props.componentsValidation[this.props.type].min && !validateCoords(components[idx]); } return validComponents.length >= this.props.componentsValidation[this.props.type].min; } @@ -325,7 +330,7 @@ class CoordinateEditor extends React.Component { addCoordPolygon = (components) => { if (this.props.type === "Polygon") { const validComponents = components.filter(validateCoords); - return components.concat([validComponents[0]]); + return components.concat([validComponents.length ? validComponents[0] : {lat: "", lon: ""}]); } return components; } diff --git a/web/client/components/mapcontrols/annotations/DropdownFeatureType.jsx b/web/client/components/mapcontrols/annotations/DropdownFeatureType.jsx index e8b499beb1..ba74a527a2 100644 --- a/web/client/components/mapcontrols/annotations/DropdownFeatureType.jsx +++ b/web/client/components/mapcontrols/annotations/DropdownFeatureType.jsx @@ -12,40 +12,53 @@ const React = require('react'); const uuidv1 = require('uuid/v1'); const assign = require('object-assign'); -const getAnnotationStyle = (type, defaultStyle = DEFAULT_ANNOTATIONS_STYLES) => { - return assign({}, defaultStyle[type], {type}); -}; const DropdownButtonT = tooltip(DropdownButton); -const DropdownFeatureType = ({onClick = () => {}, onStartDrawing = () => {}, onAddText = () => {}, onSetStyle = () => {}, bsStyle = "primary", ...props} = {}) => ( - } disabled={!!props.disabled} noCaret> +const DropdownFeatureType = ({ + onClick = () => {}, + onStartDrawing = () => {}, + onAddText = () => {}, + onSetStyle = () => {}, + titles = {}, + glyph = "", + bsStyle = "primary", + ...props +} = {}) => ( + } + disabled={!!props.disabled} + noCaret> { onClick("Point"); - onSetStyle([{...getAnnotationStyle("Point"), highlight: true, id: uuidv1()}]); + onSetStyle([{ ...DEFAULT_ANNOTATIONS_STYLES.Point, highlight: true, id: uuidv1()}]); onStartDrawing(); }}> - {props.titles.marker} + {titles.marker} { onClick("LineString"); onSetStyle( - [{...getAnnotationStyle("LineString"), highlight: true, id: uuidv1()}] + [{ ...DEFAULT_ANNOTATIONS_STYLES.LineString, highlight: true, id: uuidv1()}] .concat(getStartEndPointsForLinestring())); onStartDrawing(); }}> - {props.titles.line} + {titles.line} { onClick("Polygon"); onSetStyle([ - {...getAnnotationStyle("Polygon"), highlight: true, id: uuidv1()} + {...DEFAULT_ANNOTATIONS_STYLES.Polygon, highlight: true, id: uuidv1()} ]); onStartDrawing(); }}> - {props.titles.polygon} + {titles.polygon} { @@ -56,7 +69,7 @@ const DropdownFeatureType = ({onClick = () => {}, onStartDrawing = () => {}, onA ]); onStartDrawing(); }}> - {props.titles.text} + {titles.text} { @@ -67,7 +80,7 @@ const DropdownFeatureType = ({onClick = () => {}, onStartDrawing = () => {}, onA ]); onStartDrawing(); }}> - {props.titles.circle} + {titles.circle} ); diff --git a/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.js b/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.js index 11a0384bad..2180f6998e 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.js +++ b/web/client/components/mapcontrols/annotations/__tests__/Annotations-test.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2015-2016, GeoSolutions Sas. * All rights reserved. * diff --git a/web/client/components/mapcontrols/annotations/__tests__/AnnotationsEditor-test.js b/web/client/components/mapcontrols/annotations/__tests__/AnnotationsEditor-test.js index 4b19de324d..4d68f2e555 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/AnnotationsEditor-test.js +++ b/web/client/components/mapcontrols/annotations/__tests__/AnnotationsEditor-test.js @@ -9,8 +9,8 @@ const expect = require('expect'); const React = require('react'); const ReactDOM = require('react-dom'); -const AnnotationsEditor = require('../AnnotationsEditor'); +const AnnotationsEditor = require('../AnnotationsEditor'); const TestUtils = require('react-dom/test-utils'); const actions = { onChangeProperties: () => {}, diff --git a/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js b/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js new file mode 100644 index 0000000000..fcc5d8b106 --- /dev/null +++ b/web/client/components/mapcontrols/annotations/__tests__/CoordinatesEditor-test.js @@ -0,0 +1,716 @@ +/* + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ +const expect = require('expect'); + +const React = require('react'); +const ReactDOM = require('react-dom'); +const CoordinatesEditor = require('../CoordinatesEditor'); +const TestUtils = require('react-dom/test-utils'); + +const testHandlers = { + onChange: () => {}, + onHighlightPoint: () => {}, + onChangeRadius: () => {}, + onChangeText: () => {}, + onSetInvalidSelected: () => {} +}; + +describe("test the CoordinatesEditor Panel", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + expect.restoreSpies(); + }); + + it('CoordinatesEditor rendering with defaults', () => { + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + }); + + it('CoordinatesEditor as marker editor with base input coordinates', () => { + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const spans = TestUtils.scryRenderedDOMComponentsWithTag(editor, "span"); + expect(spans).toExist(); + expect(spans[0].innerText).toBe("annotations.editor.title.Point"); + + const exclamationMark = TestUtils.findRenderedDOMComponentWithClass(editor, "glyphicon-exclamation-mark"); + expect(exclamationMark).toExist(); + + const format = TestUtils.findRenderedDOMComponentWithClass(editor, "glyphicon-cog"); + expect(format).toExist(); + + const plus = TestUtils.scryRenderedDOMComponentsWithClass(editor, "glyphicon-plus"); + expect(plus.length).toBe(0); + }); + + it('CoordinatesEditor as Polygon editor, valid input coordinate, changing coords', () => { + const components = [{ + lat: 10, + lon: 10 + }, { + lat: 6, + lon: 6 + }, { + lat: 6, + lon: 6 + }]; + + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const input = inputs[0]; + input.value = 15; + TestUtils.Simulate.change(input); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 15, lon: 10 }); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: 15, lon: 10 }, + {lat: 6, lon: 6 }, + {lat: 6, lon: 6 }, + { lat: 15, lon: 10 } + ], undefined, undefined); + + input.value = ""; + TestUtils.Simulate.change(input); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("coords", [[10, "" ], [6, 6 ], [6, 6]]); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: "", lon: 10 }, + { lat: 6, lon: 6 }, + { lat: 6, lon: 6 }, + { lat: "", lon: 10 } + ], undefined, undefined); + }); + + it('CoordinatesEditor as LineString editor, valid input coordinate, changing coords', () => { + const components = [{ + lat: 10, + lon: 10 + }, { + lat: 6, + lon: 6 + }]; + + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const input = inputs[0]; + input.value = 15; + TestUtils.Simulate.change(input); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 15, lon: 10 }); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: 15, lon: 10 }, + {lat: 6, lon: 6 } + ], undefined, undefined); + + input.value = ""; + TestUtils.Simulate.change(input); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("coords", [[10, "" ], [6, 6 ]]); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: "", lon: 10 }, + {lat: 6, lon: 6 } + ], undefined, undefined); + }); + + it('CoordinatesEditor as Circle editor, valid input coordinate, changing coords, isMouseLeaveEnabled=true', () => { + const components = [{ + lat: 10, + lon: 10 + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputRadius = inputs[0]; + inputRadius.value = 1000; + const inputCoord = inputs[1]; + inputCoord.value = 15; + TestUtils.Simulate.change(inputCoord); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 15, lon: 10 }); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: 15, lon: 10 } + ], 1000, undefined); + + inputCoord.value = ""; + TestUtils.Simulate.change(inputCoord); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith(null); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("coords", [[10, "" ]]); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: "", lon: 10 } + ], 1000, undefined); + }); + + it('CoordinatesEditor as Circle editor, valid input coordinate, changing coords, isMouseLeaveEnabled=false', () => { + const components = [{ + lat: 10, + lon: 10 + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputRadius = inputs[0]; + inputRadius.value = 1000; + const inputCoord = inputs[1]; + + inputCoord.value = ""; + TestUtils.Simulate.change(inputCoord); + expect(spyOnHighlightPoint).toNotHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("coords", [[10, "" ]]); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([{ lat: "", lon: 10 }], 1000, undefined); + }); + + it('CoordinatesEditor as Circle editor, valid input coordinate, changing radius with valid value', () => { + const components = [{ + lat: 10, + lon: 10 + }]; + const spyOnChangeRadius = expect.spyOn(testHandlers, "onChangeRadius"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputRadius = inputs[0]; + inputRadius.value = 10000; + TestUtils.Simulate.change(inputRadius); + expect(spyOnChangeRadius).toHaveBeenCalled(); + expect(spyOnChangeRadius).toHaveBeenCalledWith(10000, [[10, 10]]); + expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); + }); + + + it('CoordinatesEditor as Circle editor, valid input coordinate, changing radius, invalid center', () => { + const components = [{ + lat: "", + lon: "" + }]; + const spyOnChangeRadius = expect.spyOn(testHandlers, "onChangeRadius"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputRadius = inputs[0]; + inputRadius.value = 10000; + TestUtils.Simulate.change(inputRadius); + expect(spyOnChangeRadius).toHaveBeenCalled(); + expect(spyOnChangeRadius).toHaveBeenCalledWith(10000, []); + expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); + }); + + + it('CoordinatesEditor as Circle editor, valid input coordinate, changing invalid radius, invalid center', () => { + const components = [{ + lat: "", + lon: "" + }]; + const spyOnChangeRadius = expect.spyOn(testHandlers, "onChangeRadius"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputRadius = inputs[0]; + inputRadius.value = ""; + TestUtils.Simulate.change(inputRadius); + expect(spyOnChangeRadius).toHaveBeenCalled(); + expect(spyOnChangeRadius).toHaveBeenCalledWith(null, [["", ""]]); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("radius", [["", ""]]); + }); + + it('CoordinatesEditor as Text editor, valid input coordinate, changing coords, isMouseLeaveEnabled=true', () => { + const components = [{ + lat: 10, + lon: 10 + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const spyOnChangeText = expect.spyOn(testHandlers, "onChangeText"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputText = inputs[0]; + inputText.value = "myTextAnnotation"; + const inputCoord = inputs[1]; + inputCoord.value = 15; + TestUtils.Simulate.change(inputCoord); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 15, lon: 10 }); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChangeText).toNotHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: 15, lon: 10 } + ], undefined, "myTextAnnotation"); + + inputCoord.value = ""; + TestUtils.Simulate.change(inputCoord); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith(null); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("coords", [[10, "" ]]); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnChangeText).toNotHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalledWith([ + { lat: "", lon: 10 } + ], undefined, "myTextAnnotation"); + }); + + it('CoordinatesEditor as Text editor, valid input coordinate, changing text, isMouseLeaveEnabled=true', () => { + const components = [{ + lat: 10, + lon: 10 + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const spyOnChangeText = expect.spyOn(testHandlers, "onChangeText"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputText = inputs[0]; + inputText.value = "my new Text Annotation"; + TestUtils.Simulate.change(inputText); + + expect(spyOnHighlightPoint).toNotHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); + expect(spyOnChange).toNotHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalledWith("my new Text Annotation", [[10, 10]]); + }); + + + it('CoordinatesEditor as Text editor, valid input coordinate, changing text, invalid point', () => { + const components = [{ + lat: "", + lon: "" + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const spyOnChangeText = expect.spyOn(testHandlers, "onChangeText"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputText = inputs[0]; + inputText.value = "my new Text Annotation"; + TestUtils.Simulate.change(inputText); + + expect(spyOnHighlightPoint).toNotHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); + expect(spyOnChange).toNotHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalledWith("my new Text Annotation", [["", ""]]); + }); + + + it('CoordinatesEditor as Text editor, valid input coordinate, changing with invalid text, invalid point', () => { + const components = [{ + lat: "", + lon: "" + }]; + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const spyOnChangeText = expect.spyOn(testHandlers, "onChangeText"); + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(editor, "input"); + expect(inputs).toExist(); + const inputText = inputs[0]; + inputText.value = ""; + TestUtils.Simulate.change(inputText); + + expect(spyOnHighlightPoint).toNotHaveBeenCalled(); + expect(spyOnChange).toNotHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalled(); + expect(spyOnChangeText).toHaveBeenCalledWith("", [["", ""]]); + expect(spyOnSetInvalidSelected).toHaveBeenCalled(); + expect(spyOnSetInvalidSelected).toHaveBeenCalledWith("text", [["", ""]]); + }); + + it('CoordinatesEditor as LineString editor, valid input coordinate, mouse enter/leave', () => { + const components = [{ + lat: 10, + lon: 10 + }, { + lat: 6, + lon: 6 + }]; + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const rows = TestUtils.scryRenderedDOMComponentsWithClass(editor, "coordinateRow"); + const firstRow = rows[0]; + TestUtils.Simulate.mouseEnter(firstRow); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({lat: 10, lon: 10}); + TestUtils.Simulate.mouseLeave(firstRow); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith(null); + }); + + it('CoordinatesEditor as LineString editor, 3 valid input coordinate, remove first row', () => { + const components = [{ + lat: 10, + lon: 10 + }, { + lat: 8, + lon: 8 + }, { + lat: 6, + lon: 6 + }]; + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + let buttons = TestUtils.scryRenderedDOMComponentsWithTag(editor, "button"); + let firstDelButton = buttons[3]; + TestUtils.Simulate.click(firstDelButton); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 8, lon: 8 }); + }); + + it('CoordinatesEditor as LineString editor, 2 valid input coordinate, cannot remove first row trash disabled', () => { + const components = [{ + lat: 10, + lon: 10 + }, { + lat: 6, + lon: 6 + }]; + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + let buttons = TestUtils.scryRenderedDOMComponentsWithTag(editor, "button"); + let firstDelButton = buttons[3]; + TestUtils.Simulate.click(firstDelButton); + expect(spyOnHighlightPoint).toNotHaveBeenCalled(); + expect(firstDelButton.disabled).toBe(true); + }); + + it('CoordinatesEditor as LineString editor, 4 rows, remove first invalid row', () => { + const components = [{ + lat: "", + lon: "" + }, { + lat: 8, + lon: 8 + }, { + lat: 8, + lon: 8 + }, { + lat: 6, + lon: 6 + }]; + const spyOnSetInvalidSelected = expect.spyOn(testHandlers, "onSetInvalidSelected"); + const spyOnHighlightPoint = expect.spyOn(testHandlers, "onHighlightPoint"); + const spyOnChange = expect.spyOn(testHandlers, "onChange"); + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + let buttons = TestUtils.scryRenderedDOMComponentsWithTag(editor, "button"); + let firstDelButton = buttons[3]; + TestUtils.Simulate.click(firstDelButton); + expect(spyOnSetInvalidSelected).toNotHaveBeenCalled(); + expect(spyOnChange).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalled(); + expect(spyOnHighlightPoint).toHaveBeenCalledWith({ lat: 8, lon: 8 }); + }); + + it('CoordinatesEditor as LineString, 4 rows, only invalid rows are not disabled', () => { + const components = [{ + lat: 5, + lon: "" + }, { + lat: 8, + lon: 8 + }, { + lat: 8, + lon: 8 + }, + { + lat: "", + lon: "" + }]; + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const buttons = TestUtils.scryRenderedDOMComponentsWithTag(editor, "button"); + expect(buttons.length).toBe(7); + expect(buttons[3].disabled).toBe(false); + expect(buttons[4].disabled).toBe(true); + expect(buttons[5].disabled).toBe(true); + expect(buttons[6].disabled).toBe(false); + }); + it('CoordinatesEditor as Polygon, 5 rows, only invalid rows are not disabled', () => { + const components = [{ + lat: 5, + lon: "" + }, { + lat: 7, + lon: 7 + }, { + lat: 8, + lon: 8 + }, { + lat: 6, + lon: 6 + }, + { + lat: "", + lon: "" + }]; + + const editor = ReactDOM.render( + , document.getElementById("container") + ); + expect(editor).toExist(); + + const buttons = TestUtils.scryRenderedDOMComponentsWithTag(editor, "button"); + expect(buttons.length).toBe(8); + expect(buttons[3].disabled).toBe(false); + expect(buttons[4].disabled).toBe(true); + expect(buttons[5].disabled).toBe(true); + expect(buttons[6].disabled).toBe(true); + expect(buttons[7].disabled).toBe(false); + }); + +}); diff --git a/web/client/components/mapcontrols/annotations/__tests__/DropdownFeatureType-test.js b/web/client/components/mapcontrols/annotations/__tests__/DropdownFeatureType-test.js new file mode 100644 index 0000000000..271d2d7872 --- /dev/null +++ b/web/client/components/mapcontrols/annotations/__tests__/DropdownFeatureType-test.js @@ -0,0 +1,123 @@ +/* + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ +const expect = require('expect'); + +const React = require('react'); +const ReactDOM = require('react-dom'); +const DropdownFeatureType = require('../DropdownFeatureType'); + +const titles = { + marker: "marker", + line: "line", + polygon: "polygon", + circle: "circle", + text: "text" +}; + +const testSpies = (spies, button) => { + expect(button).toExist(); + button.click(); + spies.forEach(spy => { + expect(spy).toHaveBeenCalled(); + }); +}; + +const resetSpies = (spies) => { + spies.forEach(spy => { + spy.restore(); + }); +}; + +describe("test the DropdownFeatureType Panel", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('DropdownFeatureType rendering with defaults', () => { + ReactDOM.render( + , document.getElementById("container") + ); + const items = document.getElementsByTagName("li"); + expect(items).toExist(); + expect(items.length).toBe(5); + const spans = document.getElementsByTagName("span"); + expect(spans).toExist(); + expect(spans.length).toBe(6); + expect(spans[0].className).toBe("glyphicon glyphicon-pencil-add"); + expect(spans[1].className).toBe("glyphicon glyphicon-point"); + expect(spans[2].className).toBe("glyphicon glyphicon-line"); + expect(spans[3].className).toBe("glyphicon glyphicon-polygon"); + expect(spans[4].className).toBe("glyphicon glyphicon-text-colour"); + expect(spans[5].className).toBe("glyphicon glyphicon-1-circle"); + }); + + it('DropdownFeatureType testing the actions', () => { + + const testHandlers = { + onClick: () => {}, + onStartDrawing: () => {}, + onSetStyle: () => {}, + onAddText: () => {} + }; + + const spyOnClick = expect.spyOn(testHandlers, 'onClick'); + const spyOnSetStyle = expect.spyOn(testHandlers, 'onSetStyle'); + const spyOnStartDrawing = expect.spyOn(testHandlers, 'onStartDrawing'); + const spyOnAddText = expect.spyOn(testHandlers, 'onAddText'); + + const spies = [spyOnClick, spyOnSetStyle, spyOnStartDrawing]; + ReactDOM.render( + , document.getElementById("container") + ); + const buttons = document.getElementsByTagName("a"); + + const marker = buttons[0]; + testSpies(spies, marker); + expect(spyOnClick).toHaveBeenCalledWith("Point"); + resetSpies(spies); + + const line = buttons[1]; + testSpies(spies, line); + expect(spyOnClick).toHaveBeenCalledWith("LineString"); + resetSpies(spies); + + const polygon = buttons[2]; + testSpies(spies, polygon); + expect(spyOnClick).toHaveBeenCalledWith("Polygon"); + resetSpies(spies); + + const text = buttons[3]; + testSpies([...spies, spyOnAddText], text); + expect(spyOnClick).toHaveBeenCalledWith("Text"); + resetSpies(spies); + + const circle = buttons[4]; + testSpies(spies, circle); + expect(spyOnClick).toHaveBeenCalledWith("Circle"); + resetSpies(spies); + }); +}); diff --git a/web/client/components/mapcontrols/annotations/__tests__/MeasureEditor-test.js b/web/client/components/mapcontrols/annotations/__tests__/MeasureEditor-test.js index 215ba1d6a3..e618f3f466 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/MeasureEditor-test.js +++ b/web/client/components/mapcontrols/annotations/__tests__/MeasureEditor-test.js @@ -1,3 +1,10 @@ +/* + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ const React = require('react'); const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-dom/test-utils'); diff --git a/web/client/components/mapcontrols/annotations/__tests__/SelectAnnotationsFile-test.js b/web/client/components/mapcontrols/annotations/__tests__/SelectAnnotationsFile-test.js index c987abbdb7..b42814fe00 100644 --- a/web/client/components/mapcontrols/annotations/__tests__/SelectAnnotationsFile-test.js +++ b/web/client/components/mapcontrols/annotations/__tests__/SelectAnnotationsFile-test.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2018, GeoSolutions Sas. * All rights reserved. * @@ -12,7 +12,6 @@ const ReactDOM = require('react-dom'); const SelectAnnotationsFile = require('../SelectAnnotationsFile'); - describe("test the SelectAnnotationsFile modal", () => { beforeEach((done) => { document.body.innerHTML = '
'; diff --git a/web/client/components/mapcontrols/measure/MeasureComponent.jsx b/web/client/components/mapcontrols/measure/MeasureComponent.jsx index f7f5a0dfe7..00c54adadb 100644 --- a/web/client/components/mapcontrols/measure/MeasureComponent.jsx +++ b/web/client/components/mapcontrols/measure/MeasureComponent.jsx @@ -9,7 +9,7 @@ const PropTypes = require('prop-types'); const React = require('react'); const {DropdownList} = require('react-widgets'); const {ButtonToolbar, Tooltip, Glyphicon, Grid, Row, Col, FormGroup} = require('react-bootstrap'); -const {isEqual, round, get, dropRight} = require('lodash'); +const {isEqual, round, get} = require('lodash'); const NumberFormat = require('../../I18N/Number'); const Message = require('../../I18N/Message'); @@ -253,9 +253,6 @@ class MeasureComponent extends React.Component { render() { const geomType = (get(this.props.measurement, 'feature.geometry.type') || '').toLowerCase(); let coords = (get(this.props.measurement, geomType.indexOf('polygon') !== -1 ? 'feature.geometry.coordinates[0]' : 'feature.geometry.coordinates') || []).map(coordinate => ({lon: coordinate[0], lat: coordinate[1]})); - if (geomType.indexOf('polygon') !== -1) { - coords = dropRight(coords); - } return ( { const coordEditorPanel = TestUtils.findRenderedDOMComponentWithClass(cmp, 'ms2-border-layout-body'); expect(coordEditorPanel).toExist(); }); + + it('rendering a coordinate editor for Polygons with 4 empty rows', () => { + let measurement = { + lineMeasureEnabled: false, + areaMeasureEnabled: true, + bearingMeasureEnabled: false, + geomType: 'Polygon', + feature: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[["", ""], ["", ""], ["", ""], ["", ""]]] + }, + properties: {} + }, + len: 0, + area: 0, + bearing: 0 + }; + let cmp = ReactDOM.render( + , document.getElementById("container") + ); + expect(cmp).toExist(); + const coordEditorPanel = TestUtils.findRenderedDOMComponentWithClass(cmp, 'ms2-border-layout-body'); + const coordinateRows = TestUtils.scryRenderedDOMComponentsWithClass(cmp, 'coordinateRow'); + expect(coordEditorPanel).toExist(); + expect(coordinateRows.length).toBe(4); + + }); }); diff --git a/web/client/components/mapcontrols/measure/measure.css b/web/client/components/mapcontrols/measure/measure.css index 4f96570d85..6cc4531db9 100644 --- a/web/client/components/mapcontrols/measure/measure.css +++ b/web/client/components/mapcontrols/measure/measure.css @@ -23,12 +23,12 @@ } .measure-value { + /* this is necessary to show the uom list correctly, + * otherwise the list will be hidden since it exceeds the modal + */ font-family: Menlo, Monaco, Consolas, Courier New, monospace; } -{ -/* this is necessary to show the uom list correctly, - * otherwise the list will be hidden since it exceeds the modal - */} + #measure .modal-body > div[role="body"] > div { overflow: visible!important; } diff --git a/web/client/components/misc/coordinateeditors/CoordinatesRow.jsx b/web/client/components/misc/coordinateeditors/CoordinatesRow.jsx index 540fd55922..ebbfe5e550 100644 --- a/web/client/components/misc/coordinateeditors/CoordinatesRow.jsx +++ b/web/client/components/misc/coordinateeditors/CoordinatesRow.jsx @@ -50,10 +50,14 @@ class CoordinatesRow extends React.Component { return ( { - if (this.props.onMouseEnter) { + if (this.props.onMouseEnter && this.props.component.lat && this.props.component.lon) { this.props.onMouseEnter(this.props.component); } - }} onMouseLeave={this.props.onMouseLeave}> + }} onMouseLeave={() => { + if (this.props.component.lat && this.props.component.lon) { + this.props.onMouseLeave(); + } + }}> {this.props.isDraggable ? this.props.connectDragSource(dragButton) : dragButton} diff --git a/web/client/components/misc/enhancers/draggableContainer.jsx b/web/client/components/misc/enhancers/draggableContainer.jsx index f2a9f9efc8..5953ac7744 100644 --- a/web/client/components/misc/enhancers/draggableContainer.jsx +++ b/web/client/components/misc/enhancers/draggableContainer.jsx @@ -8,7 +8,7 @@ module.exports = compose( dragDropContext(html5Backend), branch( ({isDraggable = true}) => isDraggable, - Component => ({onSort, isDraggable, items, ...props}) => { + Component => ({onSort, isDraggable, items = [], ...props}) => { const draggableItems = items.map((item, sortId) => ({...item, onSort, isDraggable, sortId, key: sortId})); return ; } diff --git a/web/client/components/misc/enhancers/tooltip.jsx b/web/client/components/misc/enhancers/tooltip.jsx index a88c7bbcfd..ecc25170c1 100644 --- a/web/client/components/misc/enhancers/tooltip.jsx +++ b/web/client/components/misc/enhancers/tooltip.jsx @@ -10,7 +10,7 @@ const {branch} = require('recompose'); const { Tooltip } = require('react-bootstrap'); const OverlayTrigger = require('../OverlayTrigger'); const Message = require('../../I18N/Message'); - +const {omit} = require('lodash'); /** * Tooltip enhancer. Enhances an object adding a tooltip (with i18n support). * It is applied only if props contains `tooltip` or `tooltipId`. It have to be applied to a React (functional) component @@ -34,4 +34,7 @@ module.exports = branch( trigger={tooltipTrigger} key={keyProp} placement={tooltipPosition} - overlay={{tooltipId ? : tooltip}}>)); + overlay={{tooltipId ? : tooltip}}>), + // avoid to pass non needed props + (Wrapped) => (props) => {props.children} +); diff --git a/web/client/epics/__tests__/annotations-test.js b/web/client/epics/__tests__/annotations-test.js index 6353182f75..38927ae692 100644 --- a/web/client/epics/__tests__/annotations-test.js +++ b/web/client/epics/__tests__/annotations-test.js @@ -47,6 +47,141 @@ const ft = { } }; + +const annotationsLayerWithTextFeature = { + "flat": [ + { + "id": "annotations", + "features": [ + { + type: "FeatureCollection", + features: [{ + "properties": { + "id": "1", + isText: true, + valueText: "my text" + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 1 + ] + } + }], + style: { + type: "Text", + id: "id.1.text.5", + "Text": { + color: "#FF0000", + font: "Arial 14px", + label: "my text" + } + } + } + ] + } + ] +}; + +const annotationsLayerWithCircleFeature = { + "flat": [ + { + "id": "annotations", + "features": [ + { + type: "FeatureCollection", + features: [{ + "properties": { + "id": "1", + isCircle: true, + center: [] + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 1, + 1 + ] + } + }], + style: { + type: "Circle", + id: "id.1.2.3.4.5", + "Circle": { + color: "#FF0000" + } + } + } + ] + } + ] +}; +const annotationsLayerWithLineStringFeature = { + "flat": [ + { + "id": "annotations", + "features": [ + { + type: "FeatureCollection", + features: [{ + "properties": { + "id": "1" + }, + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 1 ], + [ 12, 12 ] + ] + } + }], + style: { + type: "LineString", + id: "id.1.2.3.4.5", + "LineString": { + color: "#FF0000" + } + } + } + ] + } + ] +}; +const annotationsLayerWithPointFeatureAndSymbol = { + "flat": [ + { + "id": "annotations", + "features": [ + { + type: "FeatureCollection", + features: [{ + "properties": { + "id": "1" + }, + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 1 ], + [ 12, 12 ] + ] + }, + style: [{ + type: "Point", + id: "id.1.2.3.4.5", + iconUrl: "/path/symbol.svg", + symbolUrlCustomized: "/path/symbol.svg" + }] + }] + } + ] + } + ] +}; describe('annotations Epics', () => { let store; const defaultState = { @@ -102,6 +237,31 @@ describe('annotations Epics', () => { const action = setStyle({}); store.dispatch(action); }); + it('MAP_CONFIG_LOADED with missing annotations layer', (done) => { + const state = { + annotations: { + editing: { + style: {} + }, + originalStyle: {} + }, + layers: { + flat: [] + } + }; + testEpic(addTimeoutEpic(addAnnotationsLayerEpic, 88), 1, configureMap({}), actions => { + expect(actions.length).toBe(1); + actions.map((action) => { + switch (action.type) { + case TEST_TIMEOUT: + break; + default: + expect(false).toBe(true); + } + }); + done(); + }, state); + }); it('add annotations layer on first save', (done) => { store = mockStore({ annotations: { @@ -126,7 +286,7 @@ describe('annotations Epics', () => { store.dispatch(action); }); - it('update annotations layer', (done) => { + it('update annotations layer, MAP_CONFIG_LOADED', (done) => { let action = configureMap({}); store.subscribe(() => { @@ -151,6 +311,96 @@ describe('annotations Epics', () => { }); editAnnotation('1')(store.dispatch, store.getState); }); + it('update annotations layer with LineString Feature, with old style structure, MAP_CONFIG_LOADED', (done) => { + let action = configureMap({}); + + store = mockStore({ + annotations: { + editing: { + style: {} + }, + originalStyle: {} + }, + layers: annotationsLayerWithLineStringFeature + }); + store.subscribe(() => { + const actions = store.getActions(); + if (actions.length >= 2) { + expect(actions[1].type).toBe(UPDATE_NODE); + done(); + } + }); + + store.dispatch(action); + }); + it('update annotations layer with text Feature, with old style structure, MAP_CONFIG_LOADED', (done) => { + let action = configureMap({}); + + store = mockStore({ + annotations: { + editing: { + style: {} + }, + originalStyle: {} + }, + layers: annotationsLayerWithTextFeature + }); + store.subscribe(() => { + const actions = store.getActions(); + if (actions.length >= 2) { + expect(actions[1].type).toBe(UPDATE_NODE); + done(); + } + }); + + store.dispatch(action); + }); + it('update annotations layer with Point Feature, with new symbol style structure, MAP_CONFIG_LOADED', (done) => { + let action = configureMap({}); + + store = mockStore({ + annotations: { + editing: { + style: {} + }, + originalStyle: {} + }, + layers: annotationsLayerWithPointFeatureAndSymbol + }); + store.subscribe(() => { + const actions = store.getActions(); + if (actions.length >= 2) { + expect(actions[1].type).toBe(UPDATE_NODE); + expect(actions[1].options.features[0].features[0].style[0].symbolUrlCustomized).toBe(undefined); + done(); + } + }); + + store.dispatch(action); + }); + it('update annotations layer with Circle Feature, with old style structure, MAP_CONFIG_LOADED', (done) => { + let action = configureMap({}); + + store = mockStore({ + annotations: { + editing: { + style: {} + }, + originalStyle: {} + }, + layers: annotationsLayerWithCircleFeature + }); + store.subscribe(() => { + const actions = store.getActions(); + if (actions.length >= 2) { + expect(actions[1].type).toBe(UPDATE_NODE); + expect(actions[1].options.features[0].features[0].style.length).toBe(2); + done(); + } + }); + + store.dispatch(action); + }); /** TOFIX: . some previous test seems to break this test, uncomment the following check about CLOSE_IDENTIFY when solved. diff --git a/web/client/epics/annotations.js b/web/client/epics/annotations.js index c10f869c1b..ab3d3dd48b 100644 --- a/web/client/epics/annotations.js +++ b/web/client/epics/annotations.js @@ -45,15 +45,22 @@ const {changeDrawingStatus} = require('../actions/draw'); * @type {Object} */ +/** + * TODO test this and move it into utils +*/ const validateFeatureCollection = (feature) => { let features = feature.features.map(f => { let coords = []; + if (!f.geometry ) { + return f; + } if (f.geometry.type === "LineString" || f.geometry.type === "MultiPoint") { coords = f.geometry.coordinates.filter(validateCoordsArray); } else if (f.geometry.type === "Polygon") { - coords = [f.geometry.coordinates[0].filter(validateCoordsArray)]; + coords = f.geometry.coordinates[0] ? [f.geometry.coordinates[0].filter(validateCoordsArray)] : [[]]; } else { - coords = [f.geometry.coordinates].filter(validateCoordsArray)[0]; + coords = [f.geometry.coordinates].filter(validateCoordsArray); + coords = coords.length ? coords[0] : []; } return set("geometry.coordinates", coords, f); }); diff --git a/web/client/epics/measurement.js b/web/client/epics/measurement.js index 58df8166ca..7867d91284 100644 --- a/web/client/epics/measurement.js +++ b/web/client/epics/measurement.js @@ -9,7 +9,7 @@ const Rx = require('rxjs'); const {ADD_MEASURE_AS_ANNOTATION} = require('../actions/measurement'); const {getStartEndPointsForLinestring, DEFAULT_ANNOTATIONS_STYLES, STYLE_TEXT} = require('../utils/AnnotationsUtils'); -const {convertUom, getFormattedBearingValue} = require('../utils/MeasureUtils'); +const {convertUom, getFormattedBearingValue, validateFeatureCoordinates} = require('../utils/MeasureUtils'); const LocaleUtils = require('../utils/LocaleUtils'); const {addLayer, updateNode} = require('../actions/layers'); const {toggleControl, SET_CONTROL_PROPERTY} = require('../actions/controls'); @@ -59,7 +59,9 @@ const convertMeasureToGeoJSON = (measureGeometry, value, uom, id, measureTool, s }, { type: "Feature", - geometry: {...measureGeometry, type: isLineString(state) ? "MultiPoint" : measureGeometry.type}, + geometry: { + coordinates: validateFeatureCoordinates(measureGeometry), + type: isLineString(state) ? "MultiPoint" : measureGeometry.type}, properties: { isValidFeature: true, useGeodesicLines: isLineString(state), // this is reduntant? remove it, check in the codebase where is used and use the geom dta instad diff --git a/web/client/plugins/map/index.js b/web/client/plugins/map/index.js index 0164bc814b..aeaede8ae3 100644 --- a/web/client/plugins/map/index.js +++ b/web/client/plugins/map/index.js @@ -11,7 +11,8 @@ const React = require('react'); const {creationError, changeMapView, clickOnMap} = require('../../actions/map'); const {layerLoading, layerLoad, layerError} = require('../../actions/layers'); const {changeMousePosition} = require('../../actions/mousePosition'); -const {changeMeasurementState, changeGeometry, resetGeometry} = require('../../actions/measurement'); +const {changeMeasurementState, changeGeometry, resetGeometry, updateMeasures} = require('../../actions/measurement'); +const {measurementSelector} = require('../../selectors/measurement'); const {changeSelectionState} = require('../../actions/selection'); const {changeLocateState, onLocateError} = require('../../actions/locate'); const {changeDrawingStatus, endDrawing, setCurrentStyle, geometryChanged, drawStopped, selectFeatures, drawingFeatures} = require('../../actions/draw'); @@ -46,7 +47,8 @@ module.exports = (mapType, actions) => { const MeasurementSupport = connect((state) => ({ enabled: state.controls && state.controls.measure && state.controls.measure.enabled || false, - measurement: state.measurement || {}, + // TODO TEST selector to validate the feature: filter the coords, if length >= minValue return ft validated (close the polygon) else empty ft + measurement: measurementSelector(state), useTreshold: state.measurement && state.measurement.useTreshold || null, uom: state.measurement && state.measurement.uom || { length: {unit: 'm', label: 'm'}, @@ -54,6 +56,7 @@ module.exports = (mapType, actions) => { } }), { changeMeasurementState, + updateMeasures, resetGeometry, changeGeometry })(components.MeasurementSupport || Empty); diff --git a/web/client/reducers/__tests__/measurement-test.js b/web/client/reducers/__tests__/measurement-test.js index 4534fff220..1d38d2247f 100644 --- a/web/client/reducers/__tests__/measurement-test.js +++ b/web/client/reducers/__tests__/measurement-test.js @@ -15,6 +15,7 @@ const { changeFormatMeasurement, resetGeometry, changeGeometry, + updateMeasures, init } = require('../../actions/measurement'); const {RESET_CONTROLS, setControlProperty} = require('../../actions/controls'); @@ -162,5 +163,18 @@ describe('Test the measurement reducer', () => { expect(state.lineMeasureEnabled).toEqual(true); expect(state.geomType).toEqual("LineString"); }); + it('UPDATE_MEASURES', () => { + let state = measurement({ + geomType: "LineString", + lineMeasureEnabled: true, + areaMeasureEnabled: false, + bearingMeasureEnabled: false, + len: 0, + area: 700 + }, updateMeasures({len: 12430, area: 0})); + expect(state.len).toEqual(12430); + expect(state.area).toEqual(0); + expect(state.geomType).toEqual("LineString"); + }); }); diff --git a/web/client/reducers/annotations.js b/web/client/reducers/annotations.js index 52e850e4b6..df3d4ac139 100644 --- a/web/client/reducers/annotations.js +++ b/web/client/reducers/annotations.js @@ -23,7 +23,7 @@ const {REMOVE_ANNOTATION, CONFIRM_REMOVE_ANNOTATION, CANCEL_REMOVE_ANNOTATION, C CHANGE_FORMAT, UPDATE_SYMBOLS, ERROR_SYMBOLS } = require('../actions/annotations'); -const {validateCoordsArray, getAvailableStyler, DEFAULT_ANNOTATIONS_STYLES, convertGeoJSONToInternalModel, addIds, validateFeature, getComponents, updateAllStyles} = require('../utils/AnnotationsUtils'); +const {validateCoordsArray, getAvailableStyler, DEFAULT_ANNOTATIONS_STYLES, convertGeoJSONToInternalModel, addIds, validateFeature, getComponents, updateAllStyles, getBaseCoord} = require('../utils/AnnotationsUtils'); const {set} = require('../utils/ImmutableUtils'); const {head, findIndex, isNil, slice, castArray} = require('lodash'); @@ -37,8 +37,6 @@ const fixCoordinates = (coords, type) => { } }; -const {getBaseCoord} = require('../utils/AnnotationsUtils'); - function annotations(state = { validationErrors: {} }, action) { switch (action.type) { case CHANGED_SELECTED: { diff --git a/web/client/reducers/measurement.js b/web/client/reducers/measurement.js index ee225ff1e2..e8bca7e5ea 100644 --- a/web/client/reducers/measurement.js +++ b/web/client/reducers/measurement.js @@ -14,11 +14,13 @@ const { CHANGED_GEOMETRY, CHANGE_FORMAT, CHANGE_COORDINATES, + UPDATE_MEASURES, INIT } = require('../actions/measurement'); -const {set} = require('../utils/ImmutableUtils'); - const {TOGGLE_CONTROL, RESET_CONTROLS, SET_CONTROL_PROPERTY} = require('../actions/controls'); +const {set} = require('../utils/ImmutableUtils'); +const {isPolygon} = require('../utils/openlayers/DrawUtils'); +const {dropRight} = require('lodash'); const assign = require('object-assign'); const defaultState = { @@ -61,7 +63,14 @@ function measurement(state = defaultState, action) { } }); } - case CHANGE_MEASUREMENT_STATE: + case CHANGE_MEASUREMENT_STATE: { + let feature = action.feature; + if (isPolygon(feature)) { + /* in the state the polygon is always not closed (the feature come closed from the measureSupport) + * a selector validates the feature and it closes the polygon adding first valid coord + */ + feature = set("geometry.coordinates[0]", dropRight(feature.geometry.coordinates[0]), feature); + } return assign({}, state, { lineMeasureEnabled: action.lineMeasureEnabled, areaMeasureEnabled: action.areaMeasureEnabled, @@ -73,8 +82,19 @@ function measurement(state = defaultState, action) { bearing: action.bearing, lenUnit: action.lenUnit, areaUnit: action.areaUnit, - feature: set("properties.disabled", state.feature.properties.disabled, action.feature) + feature: set("properties.disabled", state.feature.properties.disabled, feature) }); + } + case UPDATE_MEASURES: { + const {point, len, area, bearing} = action.measures; + return { + ...state, + point, + len, + area, + bearing + }; + } case RESET_GEOMETRY: { let newState = set("feature.properties.disabled", true, state); return { @@ -170,16 +190,25 @@ function measurement(state = defaultState, action) { } case CHANGE_COORDINATES: { let coordinates = action.coordinates.map(c => ([c.lon, c.lat])); - let newState = set("feature.geometry.coordinates", state.areaMeasureEnabled ? [coordinates] : coordinates, state); - newState = set("feature.type", "Feature", newState); - newState = set("feature.properties.disabled", coordinates - .filter((c) => { - const isValid = !isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1])); - return isValid; - } - ).length !== coordinates.length, newState); - newState = set("updatedByUI", true, newState); - return set("feature.geometry.type", newState.bearingMeasureEnabled ? "LineString" : newState.geomType, newState); + // wrap in an array for polygon geom + coordinates = state.areaMeasureEnabled ? dropRight(coordinates) : coordinates; + return { + ...state, + feature: { + type: "Feature", + properties: { + disabled: coordinates.filter((c) => { + const isValid = !isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1])); + return isValid; + }).length !== coordinates.length + }, + geometry: { + type: state.bearingMeasureEnabled ? "LineString" : state.geomType, + coordinates: state.areaMeasureEnabled ? [coordinates] : coordinates + } + }, + updatedByUI: true + }; } default: return state; diff --git a/web/client/selectors/__tests__/measurement-test.js b/web/client/selectors/__tests__/measurement-test.js index 5f4a628296..75ebf35135 100644 --- a/web/client/selectors/__tests__/measurement-test.js +++ b/web/client/selectors/__tests__/measurement-test.js @@ -10,8 +10,16 @@ const expect = require('expect'); const { isCoordinateEditorEnabledSelector, - showAddAsAnnotationSelector + showAddAsAnnotationSelector, + measurementSelector, + getValidFeatureSelector } = require('../measurement'); +const { + polyFeatureNotClosedInvalid, + polyFeatureNotClosed, + lineFeature3, + lineFeatureWithoutGeom +} = require('../../test-resources/drawsupport/features'); describe('Test maptype', () => { it('test isCoordinateEditorEnabledSelector', () => { @@ -37,4 +45,37 @@ describe('Test maptype', () => { expect(retval).toExist(); expect(retval).toBe(true); }); + it('test getValidFeatureSelector no feature geom', () => { + expect(getValidFeatureSelector({ + measurement: { + feature: lineFeatureWithoutGeom + } + })).toEqual(lineFeatureWithoutGeom); + }); + it('test measurementSelector', () => { + let retval = measurementSelector({ + measurement: { + showAddAsAnnotation: true, + feature: polyFeatureNotClosedInvalid + } + }); + expect(retval).toExist(); + expect(retval.feature.geometry.coordinates).toEqual( [ [ [ 0, 1 ], [ 0, 5 ], [ 2, 1 ], [ 0, 1 ] ] ] ); + + retval = measurementSelector({ + measurement: { + showAddAsAnnotation: true, + feature: polyFeatureNotClosed + } + }); + expect(retval.feature.geometry.coordinates).toEqual( [[[3, 1], [0, 5], [3, 3], [3, 1]]] ); + + retval = measurementSelector({ + measurement: { + showAddAsAnnotation: true, + feature: lineFeature3 + } + }); + expect(retval.feature.geometry.coordinates).toEqual( lineFeature3.geometry.coordinates ); + }); }); diff --git a/web/client/selectors/measurement.js b/web/client/selectors/measurement.js index 29da494e89..c3b64015e8 100644 --- a/web/client/selectors/measurement.js +++ b/web/client/selectors/measurement.js @@ -8,6 +8,8 @@ const { isOpenlayers } = require('../selectors/maptype'); const { showCoordinateEditorSelector } = require('../selectors/controls'); +const { set } = require('../utils/ImmutableUtils'); +const { validateFeatureCoordinates } = require('../utils/MeasureUtils'); /** * selects measurement state @@ -16,7 +18,6 @@ const { showCoordinateEditorSelector } = require('../selectors/controls'); * @static */ - /** * selects the showCoordinateEditor flag from state * @memberof selectors.measurement @@ -24,10 +25,32 @@ const { showCoordinateEditorSelector } = require('../selectors/controls'); * @return {boolean} the showCoordinateEditor in the state */ const isCoordinateEditorEnabledSelector = (state) => showCoordinateEditorSelector(state) && !state.measurement.isDrawing && isOpenlayers(state); - const showAddAsAnnotationSelector = (state) => state && state.measurement && state.measurement.showAddAsAnnotation; +/** + * validating feature that can contain invalid coordinates + * polygons needs to be closed fro being drawing + * if the number of valid coords is < min for that geomType then + * return empty coordinates +*/ +const getValidFeatureSelector = (state) => { + let feature = state.measurement.feature; + if (feature.geometry) { + feature = set("geometry.coordinates", validateFeatureCoordinates(feature.geometry || {}), feature); + } + return feature; +}; + +const measurementSelector = (state) => { + return state.measurement && { + ...state.measurement, + feature: getValidFeatureSelector(state) + } || {}; +}; + module.exports = { + measurementSelector, + getValidFeatureSelector, isCoordinateEditorEnabledSelector, showAddAsAnnotationSelector }; diff --git a/web/client/test-resources/drawsupport/features.js b/web/client/test-resources/drawsupport/features.js index 3bb169b480..43761db7dc 100644 --- a/web/client/test-resources/drawsupport/features.js +++ b/web/client/test-resources/drawsupport/features.js @@ -52,5 +52,528 @@ module.exports = { }, id: "multipolygon", properties: {} + }, + lineFeatureInvalid: { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [[0, 1], [2, 4], [2, ""]] + }, + properties: {} + }, + lineFeatureInvalid2: { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [[0, 1], [2, ""]] + }, + properties: {} + }, + lineFeature: { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [[0, 1], [2, 4]] + }, + properties: {} + }, + lineFeature3: { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [[13.0, 43.0], [13.0, 40.0], [11.0, 41.0]] + }, + properties: {} + }, + lineFeatureWithoutGeom: { + type: "Feature", + geometry: undefined, + properties: {} + }, + geomCollFeature: { + type: "Feature", + geometry: { + type: "GeometryCollection", + geometries: [{ + type: "LineString", + coordinates: [[0, 1], [2, 4]] + }] + }, + properties: {} + }, + polyFeatureClosed: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[[0, 1], [0, 5], [2, 1], [0, 1]]] + }, + properties: {} + }, + polyFeatureClosed2: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[[0, 1], [0, 5], [2, 5], [0, 1]]] + }, + properties: {} + }, + polyFeatureNotClosed: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[[3, 1], [0, 5], [3, 3]]] + }, + properties: {} + }, + polyFeatureNotClosedInvalid: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[[0, 1], [0, 5], [2, 1], ["", 1]]] + }, + properties: {} + }, + polyFeatureNotClosedInvalid2: { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [[[0, 1], [0, 5], ["", 1]]] + }, + properties: {} + }, + circle: { + type: 'FeatureCollection', + id: '36835090-23ad-11e8-9839-9bab136db9a3', + newFeature: true, + properties: { + id: '36835090-23ad-11e8-9839-9bab136db9a3', + circles: [0], + isCircle: true + }, + style: { + type: 'Circle', + Circle: { + color: '#ffcc33', + opacity: 1, + weight: 3, + fillColor: '#ffffff', + fillOpacity: 0.2, + radius: 10 + } + }, + features: [{ + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [ + -7.066692092065635, + 47.17477833929903 + ], + [ + -7.070957956986268, + 47.26697075050753 + ], + [ + -7.083738716328214, + 47.35863997502234 + ], + [ + -7.104983930273322, + 47.44942710493982 + ], + [ + -7.134609753668168, + 47.53897854842588 + ], + [ + -7.172499266922562, + 47.6269473500946 + ], + [ + -7.218502937437762, + 47.71299445745591 + ], + [ + -7.272439209743323, + 47.796789930095706 + ], + [ + -7.334095222013593, + 47.87801408888858 + ], + [ + -7.403227646136091, + 47.95635860315586 + ], + [ + -7.479563648016411, + 48.03152751426181 + ], + [ + -7.562801964329729, + 48.10323819468144 + ], + [ + -7.6526140914695295, + 48.17122224206827 + ], + [ + -7.748645582001249, + 48.235226308292674 + ], + [ + -7.850517443504369, + 48.29501286380931 + ], + [ + -7.95782763428237, + 48.35036089804014 + ], + [ + -8.07015265003761, + 48.40106655672712 + ], + [ + -8.18704919524932, + 48.44694371741463 + ], + [ + -8.308055932658505, + 48.48782450436609 + ], + [ + -8.432695303955327, + 48.52355974430404 + ], + [ + -8.560475414483594, + 48.55401936438973 + ], + [ + -8.690891974524225, + 48.57909273383168 + ], + [ + -8.823430289496404, + 48.59868895043517 + ], + [ + -8.95756729122193, + 48.612737073283476 + ], + [ + -9.092773602236358, + 48.62118630258009 + ], + [ + -9.228515625000002, + 48.62400610748772 + ], + [ + -9.364257647763646, + 48.62118630258009 + ], + [ + -9.499463958778072, + 48.612737073283476 + ], + [ + -9.633600960503598, + 48.59868895043517 + ], + [ + -9.766139275475776, + 48.57909273383168 + ], + [ + -9.89655583551641, + 48.55401936438973 + ], + [ + -10.024335946044676, + 48.52355974430404 + ], + [ + -10.148975317341497, + 48.48782450436609 + ], + [ + -10.269982054750683, + 48.44694371741463 + ], + [ + -10.386878599962396, + 48.40106655672712 + ], + [ + -10.499203615717633, + 48.35036089804014 + ], + [ + -10.606513806495634, + 48.29501286380931 + ], + [ + -10.708385667998753, + 48.235226308292674 + ], + [ + -10.804417158530473, + 48.17122224206827 + ], + [ + -10.894229285670272, + 48.10323819468144 + ], + [ + -10.977467601983593, + 48.03152751426181 + ], + [ + -11.053803603863912, + 47.95635860315586 + ], + [ + -11.12293602798641, + 47.87801408888858 + ], + [ + -11.184592040256682, + 47.796789930095706 + ], + [ + -11.238528312562241, + 47.71299445745591 + ], + [ + -11.284531983077443, + 47.6269473500946 + ], + [ + -11.322421496331836, + 47.53897854842588 + ], + [ + -11.352047319726681, + 47.44942710493982 + ], + [ + -11.373292533671789, + 47.35863997502234 + ], + [ + -11.386073293013734, + 47.26697075050753 + ], + [ + -11.390339157934367, + 47.17477833929903 + ], + [ + -11.386073293013734, + 47.08242559504841 + ], + [ + -11.373292533671789, + 46.990277901535556 + ], + [ + -11.35204731972668, + 46.8987017170481 + ], + [ + -11.322421496331836, + 46.808063084694076 + ], + [ + -11.284531983077443, + 46.718726115193576 + ], + [ + -11.238528312562241, + 46.63105144927073 + ], + [ + -11.184592040256682, + 46.545394707297326 + ], + [ + -11.12293602798641, + 46.46210493431348 + ], + [ + -11.053803603863912, + 46.381523048960865 + ], + [ + -10.977467601983593, + 46.303980305201314 + ], + [ + -10.894229285670272, + 46.22979677595146 + ], + [ + -10.804417158530473, + 46.15927986793656 + ], + [ + -10.708385667998753, + 46.09272287714786 + ], + [ + -10.60651380649563, + 46.03040359427646 + ], + [ + -10.499203615717633, + 45.972582969388 + ], + [ + -10.386878599962396, + 45.919503844897314 + ], + [ + -10.269982054750683, + 45.871389765601215 + ], + [ + -10.148975317341497, + 45.828443874131516 + ], + [ + -10.024335946044676, + 45.79084789970411 + ], + [ + -9.89655583551641, + 45.75876124746622 + ], + [ + -9.766139275475776, + 45.73232019509079 + ], + [ + -9.633600960503598, + 45.711637202538626 + ], + [ + -9.499463958778076, + 45.696800340115416 + ], + [ + -9.364257647763646, + 45.6878728390993 + ], + [ + -9.228515625000002, + 45.684892768315834 + ], + [ + -9.092773602236358, + 45.6878728390993 + ], + [ + -8.95756729122193, + 45.696800340115416 + ], + [ + -8.823430289496406, + 45.711637202538626 + ], + [ + -8.690891974524225, + 45.73232019509079 + ], + [ + -8.560475414483594, + 45.75876124746622 + ], + [ + -8.43269530395533, + 45.79084789970411 + ], + [ + -8.308055932658508, + 45.828443874131516 + ], + [ + -8.187049195249319, + 45.871389765601215 + ], + [ + -8.07015265003761, + 45.919503844897314 + ], + [ + -7.95782763428237, + 45.972582969388 + ], + [ + -7.850517443504371, + 46.03040359427646 + ], + [ + -7.7486455820012505, + 46.09272287714786 + ], + [ + -7.652614091469531, + 46.15927986793656 + ], + [ + -7.562801964329729, + 46.22979677595146 + ], + [ + -7.479563648016411, + 46.3039803052013 + ], + [ + -7.403227646136092, + 46.381523048960865 + ], + [ + -7.334095222013593, + 46.46210493431348 + ], + [ + -7.272439209743323, + 46.545394707297326 + ], + [ + -7.218502937437762, + 46.63105144927073 + ], + [ + -7.172499266922562, + 46.718726115193576 + ], + [ + -7.134609753668168, + 46.808063084694076 + ], + [ + -7.104983930273322, + 46.8987017170481 + ], + [ + -7.083738716328214, + 46.990277901535556 + ], + [ + -7.070957956986268, + 47.08242559504841 + ], + [ + -7.066692092065635, + 47.17477833929903 + ] + ] + ] + } + }] } }; diff --git a/web/client/themes/default/less/annotations.less b/web/client/themes/default/less/annotations.less index c0729218be..b3fb770a8b 100644 --- a/web/client/themes/default/less/annotations.less +++ b/web/client/themes/default/less/annotations.less @@ -256,56 +256,6 @@ margin-top: 0px; } } - /*display: flex; - flex-direction: column; - span { - flex: 1 1 0%; - display: flex; - .mapstore-annotations-info-viewer { - width: 100%; - .mapstore-annotations-info-viewer-items { - display: flex; - .container-fluid { - flex: 1; - display: flex; - flex-direction: column; - .row { - flex: 1; - display: flex; - flex-direction: column; - &:first-child { - flex: 1; - display: flex; - flex-direction: column; - &:first-child { - margin-bottom: 15px; - } - span:nth-child(2){ - flex: 1; - display: flex; - flex-direction: column; - div:nth-child(2){ - flex: 1; - display: flex; - .quill{ - flex: 1; - display: flex; - flex-direction: column; - .ql-container { - flex: 1; - display: flex; - flex-direction: column; - } - } - } - } - } - } - } - } - } - }*/ - .msSideGrid { height: ~'calc(100% - 161px)'; diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index f2733c7593..a46be9f315 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -890,6 +890,7 @@ "MultiPoint": "LineString-Editor", "Text": "Text-Editor" }, + "center": "Center", "add": "Fügen Sie neue Koordinaten hinzu", "addByClick": "Fügen Sie neue Koordinaten hinzu, indem Sie auf die Plus-Schaltfläche oder auf die Karte klicken", "valid": "Geometrie ist gültig", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 01c5ec6ac9..2f3b3c7bea 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -890,6 +890,7 @@ "MultiPoint": "LineString editor", "Text": "Text editor" }, + "center": "Center", "add": "Add new coordinates", "addByClick": "Add new coordinates by clicking the plus button or on the map", "valid": "Geometry is valid", diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 0473c1725c..90f36a8c8e 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -890,6 +890,7 @@ "MultiPoint": "Editor LineString", "Text": "Editor de texto" }, + "center": "Centro", "add": "Agregar nuevas coordenadas", "addByClick": "Agregue nuevas coordenadas haciendo clic en el botón más o en el mapa", "valid": "La geometría es válida", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 6a59eb56a3..9ccb91cd83 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -891,6 +891,7 @@ "MultiPoint": "Éditeur de ligne", "Text": "Éditeur de texte" }, + "center": "Centre", "add": "Ajouter de nouvelles coordonnées", "addByClick": "Ajouter de nouvelles coordonnées en cliquant sur le bouton plus ou sur la carte", "valid": "La géométrie est valide", diff --git a/web/client/translations/data.hr-HR b/web/client/translations/data.hr-HR index 7e738ec6ca..592aef334c 100644 --- a/web/client/translations/data.hr-HR +++ b/web/client/translations/data.hr-HR @@ -919,6 +919,7 @@ "MultiPoint": "LineString editor", "Text": "Text editor" }, + "center": "Center", "add": "Add new coordinates", "addByClick": "Add new coordinates by clicking the plus button or on the map", "valid": "Geometry is valid", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index e98bfc9320..5500b58fab 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -890,6 +890,7 @@ "MultiPoint": "Editor della linea", "Text": "Editor del testo" }, + "center": "Centro", "add": "Aggiungi una nuova coordinata", "addByClick": "Aggiungi nuove coordinate cliccando sul pulsante + o sulla mappa", "valid": "La geometria è valida", diff --git a/web/client/translations/data.nl-NL b/web/client/translations/data.nl-NL index 5deffd305f..e938da71bf 100644 --- a/web/client/translations/data.nl-NL +++ b/web/client/translations/data.nl-NL @@ -858,6 +858,7 @@ "MultiPoint": "LineString editor", "Text": "Text editor" }, + "center": "Center", "add": "Add new coordinates", "addByClick": "Add new coordinates by clicking the plus button or on the map", "valid": "Geometry is valid", diff --git a/web/client/translations/data.pt-PT b/web/client/translations/data.pt-PT index 6dae0fa092..18efc121bc 100644 --- a/web/client/translations/data.pt-PT +++ b/web/client/translations/data.pt-PT @@ -920,6 +920,7 @@ "MultiPoint": "LineString editor", "Text": "Text editor" }, + "center": "Center", "add": "Add new coordinates", "addByClick": "Add new coordinates by clicking the plus button or on the map", "valid": "Geometry is valid", diff --git a/web/client/translations/data.zh-ZH b/web/client/translations/data.zh-ZH index fd1f891877..9af44bf00e 100644 --- a/web/client/translations/data.zh-ZH +++ b/web/client/translations/data.zh-ZH @@ -869,6 +869,7 @@ "MultiPoint": "LineString editor", "Text": "Text editor" }, + "center": "Center", "add": "Add new coordinates", "addByClick": "Add new coordinates by clicking the plus button or on the map", "valid": "Geometry is valid", diff --git a/web/client/utils/AnnotationsUtils.js b/web/client/utils/AnnotationsUtils.js index ffc311baf1..0c1473ad7e 100644 --- a/web/client/utils/AnnotationsUtils.js +++ b/web/client/utils/AnnotationsUtils.js @@ -464,6 +464,12 @@ const AnnotationsUtils = { formatCoordinates: (coords = [[]]) => { return coords.map(c => ({lat: c && c[1], lon: c && c[0]})); }, + getBaseCoord: (type) => { + switch (type) { + case "Polygon": case "LineString": case "MultiPoint": return []; + default: return [[{lat: "", lon: ""}]]; + } + }, getComponents: ({type, coordinates}) => { switch (type) { case "Polygon": { @@ -532,12 +538,6 @@ const AnnotationsUtils = { } return AnnotationsUtils.validateCoordinates({components, remove, type}); }, - getBaseCoord: (type) => { - switch (type) { - case "Polygon": case "LineString": case "MultiPoint": return []; - default: return [[]]; - } - }, updateAllStyles: (ftColl = {}, newStyle = {}) => { if (ftColl.features && ftColl.features.length) { return { diff --git a/web/client/utils/DrawSupportUtils.jsx b/web/client/utils/DrawSupportUtils.jsx index afeed28aca..7b5acb183d 100644 --- a/web/client/utils/DrawSupportUtils.jsx +++ b/web/client/utils/DrawSupportUtils.jsx @@ -56,7 +56,8 @@ const transformPolygonToCircle = (feature, mapCrs) => { if (!feature.getGeometry() || feature.getGeometry().getType() !== "Polygon" || feature.getProperties().center && feature.getProperties().center.length === 0) { return feature; } - if (feature.getProperties() && feature.getProperties().isCircle) { + if (feature.getProperties() && feature.getProperties().isCircle && feature.getProperties().center && feature.getProperties().center[0] && feature.getProperties().center[1]) { + // center must be a valid point const extent = feature.getGeometry().getExtent(); let center; if (feature.getProperties().center) { diff --git a/web/client/utils/MeasureUtils.js b/web/client/utils/MeasureUtils.js index 7ec072c37d..8fa5a53833 100644 --- a/web/client/utils/MeasureUtils.js +++ b/web/client/utils/MeasureUtils.js @@ -141,16 +141,44 @@ function convertUom(value, source = "m", dest = "m") { return value; } -const isValidGeometry = ({coordinates, type}) => { + +const validateCoord = c => (!isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1]))); + +/** + * validate a geometry feature, + * if invalid return an empty one +*/ +const validateFeatureCoordinates = ({coordinates, type} = {}) => { + let filteredCoords = coordinates; + if (type === "LineString") { + filteredCoords = coordinates.filter(validateCoord); + if (filteredCoords.length < 2) { + // if invalid return empty LineString + return []; + } + } else if (type === "Polygon") { + filteredCoords = head(coordinates).filter(validateCoord); + if (filteredCoords.length < 3) { + // if invalid return empty Polygon + return [[]]; + } + // close polygon + filteredCoords = [filteredCoords.concat([head(filteredCoords)])]; + } + return filteredCoords; +}; + +const isValidGeometry = ({coordinates, type} = {}) => { if (!type || !coordinates || coordinates && isArray(coordinates) && coordinates.length === 0) { return false; } - let coords = type === "Polygon" ? head(coordinates) : coordinates; - let filteredCoords = coords.filter(c => !isNaN(parseFloat(c[0])) && !isNaN(parseFloat(c[1]))); - return filteredCoords.length > 0 && filteredCoords.length === coords.length; + let validatedCoords = validateFeatureCoordinates({coordinates, type}); + validatedCoords = type === "Polygon" ? head(validatedCoords) : validatedCoords; + return validatedCoords.length > 0; }; module.exports = { + validateFeatureCoordinates, isValidGeometry, convertUom, getFormattedBearingValue, diff --git a/web/client/utils/__tests__/AnnotationsUtils-test.js b/web/client/utils/__tests__/AnnotationsUtils-test.js index 3c58719aeb..8fc679bb70 100644 --- a/web/client/utils/__tests__/AnnotationsUtils-test.js +++ b/web/client/utils/__tests__/AnnotationsUtils-test.js @@ -478,11 +478,12 @@ describe('Test the AnnotationsUtils', () => { expect(coordToArray({lon: 2, lat: 1})[0]).toBe(2); expect(coordToArray({lon: 2, lat: 1})[1]).toBe(1); }); - it('test getBaseCoord defaults', () => { + it('test getBaseCoord', () => { expect(getBaseCoord().length).toBe(1); - expect(getBaseCoord()[0].length).toBe(0); + expect(getBaseCoord()[0].length).toBe(1); expect(getBaseCoord("Polygon").length).toBe(0); expect(getBaseCoord("LineString").length).toBe(0); + expect(getBaseCoord("MultiPoint").length).toBe(0); }); it('test validateText defaults', () => { let components = [{lat: 4, lon: 4}]; diff --git a/web/client/utils/__tests__/MeasureUtils-test.js b/web/client/utils/__tests__/MeasureUtils-test.js index 06cfb125db..acc5fbe21b 100644 --- a/web/client/utils/__tests__/MeasureUtils-test.js +++ b/web/client/utils/__tests__/MeasureUtils-test.js @@ -12,6 +12,14 @@ const { convertUom, isValidGeometry } = require('../MeasureUtils'); +const { + lineFeature, + lineFeatureInvalid, + lineFeatureInvalid2, + polyFeatureClosed, + polyFeatureNotClosedInvalid, + polyFeatureNotClosedInvalid2 +} = require('../../test-resources/drawsupport/features'); describe('MeasureUtils', () => { @@ -88,20 +96,28 @@ describe('MeasureUtils', () => { val = getFormattedBearingValue(281.111); expect(val).toBe("N 78° 53' 20'' W"); }); - it('isValidGeometry with a valid geom', () => { - const geometry = { - type: "LineString", - coordinates: [[1, 1], [2, 2]] - }; - const isValid = isValidGeometry(geometry); + it('testing isValidGeometry() with all valid coords (line geom)', () => { + const isValid = isValidGeometry(lineFeature.geometry); + expect(isValid).toBe(true); + }); + it('testing isValidGeometry() with some invalid coords (line geom) 2 point valid', () => { + const isValid = isValidGeometry(lineFeatureInvalid.geometry); + expect(isValid).toBe(true); + }); + it('testing isValidGeometry() with some invalid coords (line geom) 1 point valid', () => { + const isValid = isValidGeometry(lineFeatureInvalid2.geometry); + expect(isValid).toBe(false); + }); + it('testing isValidGeometry() with all valid coords (polygon geom)', () => { + const isValid = isValidGeometry(polyFeatureClosed.geometry); + expect(isValid).toBe(true); + }); + it('testing isValidGeometry() with some invalid coords (polygon geom) 3 point valid', () => { + const isValid = isValidGeometry(polyFeatureNotClosedInvalid.geometry); expect(isValid).toBe(true); }); - it('isValidGeometry with an invalid geom', () => { - const geometry = { - type: "LineString", - coordinates: [[1, 1], [2, 2], ["", 2]] - }; - const isValid = isValidGeometry(geometry); + it('testing isValidGeometry() with some invalid coords (polygon geom) only 2 point valid', () => { + const isValid = isValidGeometry(polyFeatureNotClosedInvalid2.geometry); expect(isValid).toBe(false); }); diff --git a/web/client/utils/openlayers/DrawUtils.js b/web/client/utils/openlayers/DrawUtils.js new file mode 100644 index 0000000000..187fae6130 --- /dev/null +++ b/web/client/utils/openlayers/DrawUtils.js @@ -0,0 +1,29 @@ +/** + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ + +let ol = require('openlayers'); +module.exports = { + createOLGeometry: ({type, coordinates, radius, center} = {}) => { + let geometry; + switch (type) { + case "Point": { geometry = new ol.geom.Point(coordinates ? coordinates : []); break; } + case "LineString": { geometry = new ol.geom.LineString(coordinates ? coordinates : []); break; } + case "MultiPoint": { geometry = new ol.geom.MultiPoint(coordinates ? coordinates : []); break; } + case "MultiLineString": { geometry = new ol.geom.MultiLineString(coordinates ? coordinates : []); break; } + case "MultiPolygon": { geometry = new ol.geom.MultiPolygon(coordinates ? coordinates : []); break; } + // defaults is Polygon / Circle + default: { geometry = radius && center ? + ol.geom.Polygon.fromCircle(new ol.geom.Circle([center.x, center.y], radius), 100) : new ol.geom.Polygon(coordinates ? coordinates : []); + } + } + return geometry; + }, + isPolygon: (feature = {}) => { + return feature && feature.geometry && feature.geometry.type === "Polygon"; + } +}; diff --git a/web/client/utils/openlayers/__tests__/DrawUtils-test.js b/web/client/utils/openlayers/__tests__/DrawUtils-test.js new file mode 100644 index 0000000000..7a210e42f5 --- /dev/null +++ b/web/client/utils/openlayers/__tests__/DrawUtils-test.js @@ -0,0 +1,122 @@ +/** + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ + +const {createOLGeometry, isPolygon} = require('../DrawUtils'); +const expect = require('expect'); + + +describe('DrawUtils openlayers', () => { + + it('test createOLGeometry defaults', () => { + const geom = createOLGeometry(); + expect(geom).toExist(); + expect(geom.getType()).toBe("Polygon"); + }); + it('test createOLGeometry LineString', () => { + // empty geom + const type = "LineString"; + const coordinates = [[1, 1], [0, 0]]; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, coordinates}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(coordinates.length); + expect(geom.getCoordinates()).toEqual(coordinates); + }); + it('test createOLGeometry Point', () => { + // empty geom + const type = "Point"; + const coordinates = [1, 1]; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, coordinates}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(coordinates.length); + expect(geom.getCoordinates()).toEqual(coordinates); + }); + it('test createOLGeometry MultiPoint', () => { + // empty geom + const type = "MultiPoint"; + const coordinates = [[1, 1], [0, 0]]; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, coordinates}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(coordinates.length); + expect(geom.getCoordinates()).toEqual(coordinates); + }); + it('test createOLGeometry MultiLineString', () => { + // empty geom + const type = "MultiLineString"; + const coordinates = [ [[1, 1], [0, 0]], [[21, 21], [20, 20]]]; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, coordinates}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(coordinates.length); + expect(geom.getCoordinates()).toEqual(coordinates); + }); + it('test createOLGeometry MultiPolygon', () => { + // empty geom + const type = "MultiPolygon"; + const coordinates = [[ [[1, 1], [0, 0]], [[21, 21], [20, 20]]]]; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, coordinates}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(coordinates.length); + expect(geom.getCoordinates()).toEqual(coordinates); + }); + it('test createOLGeometry Circle', () => { + // empty geom + const type = "Polygon"; + const radius = 100; + const center = {x: 1, y: 1}; + + let geom = createOLGeometry({type}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(0); + + geom = createOLGeometry({type, radius, center}); + expect(geom).toExist(); + expect(geom.getType()).toBe(type); + expect(geom.getCoordinates().length).toBe(1); + expect(geom.getCoordinates()[0].length).toBe(101); + }); + + it('test isPolygon', () => { + expect(isPolygon()).toBeFalsy(); + }); + +});