diff --git a/web/client/actions/draw.js b/web/client/actions/draw.js index 8f480da46c..7d85b9c69a 100644 --- a/web/client/actions/draw.js +++ b/web/client/actions/draw.js @@ -8,6 +8,7 @@ const CHANGE_DRAWING_STATUS = 'CHANGE_DRAWING_STATUS'; const END_DRAWING = 'END_DRAWING'; +const SET_CURRENT_STYLE = 'SET_CURRENT_STYLE'; function changeDrawingStatus(status, method, owner, features, options) { return { @@ -28,9 +29,18 @@ function endDrawing(geometry, owner) { }; } +function setCurrentStyle(style) { + return { + type: SET_CURRENT_STYLE, + currentStyle: style + }; +} + module.exports = { CHANGE_DRAWING_STATUS, END_DRAWING, + SET_CURRENT_STYLE, changeDrawingStatus, - endDrawing + endDrawing, + setCurrentStyle }; diff --git a/web/client/actions/map.js b/web/client/actions/map.js index b658b02f41..c68fb147c5 100644 --- a/web/client/actions/map.js +++ b/web/client/actions/map.js @@ -12,11 +12,20 @@ const CHANGE_MOUSE_POINTER = 'CHANGE_MOUSE_POINTER'; const CHANGE_ZOOM_LVL = 'CHANGE_ZOOM_LVL'; const PAN_TO = 'PAN_TO'; const ZOOM_TO_EXTENT = 'ZOOM_TO_EXTENT'; +const ZOOM_TO_POINT = 'ZOOM_TO_POINT'; const CHANGE_MAP_CRS = 'CHANGE_MAP_CRS'; const CHANGE_MAP_SCALES = 'CHANGE_MAP_SCALES'; const CHANGE_MAP_STYLE = 'CHANGE_MAP_STYLE'; const CHANGE_ROTATION = 'CHANGE_ROTATION'; +function zoomToPoint(pos, zoom, crs) { + return { + type: ZOOM_TO_POINT, + pos, + zoom, + crs + }; +} function changeMapView(center, zoom, bbox, size, mapStateSource, projection, viewerOptions) { return { @@ -108,6 +117,7 @@ module.exports = { CHANGE_MAP_SCALES, CHANGE_MAP_STYLE, CHANGE_ROTATION, + ZOOM_TO_POINT, changeMapView, clickOnMap, changeMousePointer, @@ -117,5 +127,6 @@ module.exports = { zoomToExtent, panTo, changeMapStyle, - changeRotation + changeRotation, + zoomToPoint }; diff --git a/web/client/actions/mapInfo.js b/web/client/actions/mapInfo.js index 0182edcfb2..c161051bc1 100644 --- a/web/client/actions/mapInfo.js +++ b/web/client/actions/mapInfo.js @@ -215,5 +215,7 @@ module.exports = { showMapinfoRevGeocode, getVectorInfo, noQueryableLayers, - clearWarning + clearWarning, + errorFeatureInfo, + loadFeatureInfo }; diff --git a/web/client/actions/measurement.js b/web/client/actions/measurement.js index a460e30404..7fec02f107 100644 --- a/web/client/actions/measurement.js +++ b/web/client/actions/measurement.js @@ -27,6 +27,7 @@ function changeMeasurement(measurement) { function changeMeasurementState(measureState) { return { type: CHANGE_MEASUREMENT_STATE, + pointMeasureEnabled: measureState.pointMeasureEnabled, lineMeasureEnabled: measureState.lineMeasureEnabled, areaMeasureEnabled: measureState.areaMeasureEnabled, bearingMeasureEnabled: measureState.bearingMeasureEnabled, diff --git a/web/client/actions/search.js b/web/client/actions/search.js index b100a9d5b1..229549590f 100644 --- a/web/client/actions/search.js +++ b/web/client/actions/search.js @@ -17,6 +17,7 @@ const TEXT_SEARCH_NESTED_SERVICES_SELECTED = 'TEXT_SEARCH_NESTED_SERVICE_SELECTE const TEXT_SEARCH_ERROR = 'TEXT_SEARCH_ERROR'; const TEXT_SEARCH_CANCEL_ITEM = 'TEXT_SEARCH_CANCEL_ITEM'; const TEXT_SEARCH_ITEM_SELECTED = 'TEXT_SEARCH_ITEM_SELECTED'; +const TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE = 'TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE'; /** * updates the results of the search result loaded * @memberof actions.search @@ -92,10 +93,11 @@ function resetSearch() { * @memberof actions.search * @param {object} itemPosition */ -function addMarker(itemPosition) { +function addMarker(itemPosition, itemText) { return { type: TEXT_SEARCH_ADD_MARKER, - markerPosition: itemPosition + markerPosition: itemPosition, + markerLabel: itemText }; } @@ -157,6 +159,18 @@ function cancelSelectedItem(item) { }; } +/** + * Highlights the given feature + * @memberof actions.search + * @param {object} feature the feature to highlight + */ +function setHighlightedFeature(feature) { + return { + type: TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE, + highlightedFeature: feature + }; +} + /** * Actions for search * @name actions.search @@ -174,6 +188,7 @@ module.exports = { TEXT_SEARCH_ITEM_SELECTED, TEXT_SEARCH_NESTED_SERVICES_SELECTED, TEXT_SEARCH_CANCEL_ITEM, + TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE, searchTextLoading, searchResultError, searchResultLoaded, @@ -184,5 +199,6 @@ module.exports = { searchTextChanged, selectNestedService, selectSearchItem, - cancelSelectedItem + cancelSelectedItem, + setHighlightedFeature }; diff --git a/web/client/actions/selection.js b/web/client/actions/selection.js new file mode 100644 index 0000000000..dc7bc681d6 --- /dev/null +++ b/web/client/actions/selection.js @@ -0,0 +1,23 @@ +/** + * Copyright 2017, Sourcepole AG. + * 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 CHANGE_SELECTION_STATE = 'CHANGE_SELECTION_STATE'; + +function changeSelectionState(selectionState) { + return { + type: CHANGE_SELECTION_STATE, + geomType: selectionState.geomType, + point: selectionState.point, + line: selectionState.line, + polygon: selectionState.polygon + }; +} + +module.exports = { + CHANGE_SELECTION_STATE, + changeSelectionState +}; diff --git a/web/client/components/buttons/ZoomButton.jsx b/web/client/components/buttons/ZoomButton.jsx index 0df93c6129..afee8d505f 100644 --- a/web/client/components/buttons/ZoomButton.jsx +++ b/web/client/components/buttons/ZoomButton.jsx @@ -39,7 +39,8 @@ const ZoomButton = React.createClass({ minZoom: 0, maxZoom: 28, onZoom: () => {}, - bsStyle: "default" + bsStyle: "default", + style: {} }; }, render() { diff --git a/web/client/components/data/identify/Identify.jsx b/web/client/components/data/identify/Identify.jsx index 1d39bbb6bd..3aea10af2e 100644 --- a/web/client/components/data/identify/Identify.jsx +++ b/web/client/components/data/identify/Identify.jsx @@ -226,7 +226,7 @@ const Identify = React.createClass({ }, render() { if (this.props.enabled && this.props.requests.length !== 0) { - return this.props.draggable ? ( + return this.props.draggable && this.props.asPanel ? ( {this.renderContent()} diff --git a/web/client/components/map/openlayers/DrawSupport.jsx b/web/client/components/map/openlayers/DrawSupport.jsx index ce0bbd93c7..59936e1397 100644 --- a/web/client/components/map/openlayers/DrawSupport.jsx +++ b/web/client/components/map/openlayers/DrawSupport.jsx @@ -8,9 +8,10 @@ const React = require('react'); const ol = require('openlayers'); -const {concat} = require('lodash'); +const {concat, head} = require('lodash'); const assign = require('object-assign'); +const uuid = require('uuid'); const DrawSupport = React.createClass({ propTypes: { @@ -38,12 +39,20 @@ const DrawSupport = React.createClass({ }; }, componentWillReceiveProps(newProps) { + if (this.drawLayer) { + this.updateFeatureStyles(newProps.features); + } + + if (!newProps.drawStatus && this.selectInteraction) { + this.selectInteraction.getFeatures().clear(); + } + switch (newProps.drawStatus) { case ("create"): this.addLayer(newProps); break; case ("start"): - this.addDrawInteraction(newProps); + this.addInteractions(newProps); break; case ("stop"): this.removeDrawInteraction(); @@ -60,40 +69,32 @@ const DrawSupport = React.createClass({ }, render() { return null; + }, + updateFeatureStyles(features) { + if (features && features.length > 0) { + features.map(f => { + if (f.style) { + let olFeature = this.toOlFeature(f); + if (olFeature) { + olFeature.setStyle(this.toOlStyle(f.style, f.selected)); + } + } + }); + } }, addLayer: function(newProps, addInteraction) { - var source; - var vector; this.geojson = new ol.format.GeoJSON(); - - // create a layer to draw on - source = new ol.source.Vector(); - vector = new ol.layer.Vector({ - source: source, + this.drawSource = new ol.source.Vector(); + this.drawLayer = new ol.layer.Vector({ + source: this.drawSource, zIndex: 1000000, - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 255, 255, 0.2)' - }), - stroke: new ol.style.Stroke({ - color: '#ffcc33', - width: 2 - }), - image: new ol.style.Circle({ - radius: 7, - fill: new ol.style.Fill({ - color: '#ffcc33' - }) - }) - }) + style: this.toOlStyle(newProps.style) }); - this.props.map.addLayer(vector); + this.props.map.addLayer(this.drawLayer); - this.drawSource = source; - this.drawLayer = vector; if (addInteraction) { - this.addDrawInteraction(newProps); + this.addInteractions(newProps); } this.addFeatures(newProps.features || []); @@ -132,18 +133,38 @@ const DrawSupport = React.createClass({ this.addFeatures(newProps.features || []); } }, - addDrawInteraction: function(newProps) { - let draw; - let geometryType = newProps.drawMethod; - - if (!this.drawLayer) { - this.addLayer(newProps); - } - + addDrawInteraction(drawMethod, startingPoint, maxPoints) { if (this.drawInteraction) { this.removeDrawInteraction(); } - let features = new ol.Collection(); + this.drawInteraction = new ol.interaction.Draw(this.drawPropertiesForGeometryType(drawMethod, maxPoints)); + + this.drawInteraction.on('drawstart', function(evt) { + this.sketchFeature = evt.feature; + if (this.selectInteraction) { + this.selectInteraction.getFeatures().clear(); + this.selectInteraction.setActive(false); + } + }, this); + + this.drawInteraction.on('drawend', function(evt) { + this.sketchFeature = evt.feature; + this.sketchFeature.set('id', uuid.v1()); + const feature = this.fromOLFeature(this.sketchFeature, this.props.drawMethod, startingPoint); + + this.props.onEndDrawing(feature, this.props.drawOwner); + if (this.props.options.stopAfterDrawing) { + this.props.onChangeDrawingStatus('stop', this.props.drawMethod, this.props.drawOwner, this.props.features.concat([feature])); + } + if (this.selectInteraction) { + this.selectInteraction.setActive(true); + } + }, this); + + this.props.map.addInteraction(this.drawInteraction); + this.setDoubleClickZoomEnabled(false); + }, + drawPropertiesForGeometryType(geometryType, maxPoints) { let drawBaseProps = { source: this.drawSource, type: /** @type {ol.geom.GeometryType} */ geometryType, @@ -166,7 +187,7 @@ const DrawSupport = React.createClass({ }) }) }), - features: features, + features: new ol.Collection(), condition: ol.events.condition.always }; // Prepare the properties for the BBOX drawing @@ -215,7 +236,7 @@ const DrawSupport = React.createClass({ } case "LineString": { roiProps.type = "LineString"; - roiProps.maxPoints = newProps.options.maxPoints; + roiProps.maxPoints = maxPoints; roiProps.geometryFunction = function(coordinates, geometry) { let geom = geometry; if (!geom) { @@ -240,77 +261,216 @@ const DrawSupport = React.createClass({ } default : return {}; } - let drawProps = assign({}, drawBaseProps, roiProps); - - // create an interaction to draw with - draw = new ol.interaction.Draw(drawProps); + return assign({}, drawBaseProps, roiProps); + }, + setDoubleClickZoomEnabled(enabled) { + let interactions = this.props.map.getInteractions(); + for (let i = 0; i < interactions.getLength(); i++) { + let interaction = interactions.item(i); + if (interaction instanceof ol.interaction.DoubleClickZoom) { + interaction.setActive(enabled); + break; + } + } + }, + updateFeatureExtent(event) { + const movedFeatures = event.features.getArray(); + const updatedFeatures = this.props.features.map((f) => { + const moved = head(movedFeatures.filter((mf) => this.fromOLFeature(mf, this.props.drawMethod).id === f.id)); + return moved ? assign({}, f, { + geometry: moved.geometry, + center: moved.center, + extent: moved.extent, + coordinate: moved.coordinates, + radius: moved.radius + }) : f; + }); - draw.on('drawstart', function(evt) { - this.sketchFeature = evt.feature; - this.drawSource.clear(); - }, this); + this.props.onChangeDrawingStatus('replace', this.props.drawMethod, this.props.drawOwner, updatedFeatures); + }, + addInteractions: function(newProps) { + if (!this.drawLayer) { + this.addLayer(newProps); + } + this.addDrawInteraction(newProps.drawMethod, newProps.options.startingPoint, newProps.options.maxPoints); + if (newProps.options && newProps.options.editEnabled) { + this.addSelectInteraction(); - draw.on('drawend', function(evt) { - this.sketchFeature = evt.feature; - let startingPoint = newProps.options.startingPoint; - let drawnGeometry = this.sketchFeature.getGeometry(); - let radius; - let extent = drawnGeometry.getExtent(); - let type = drawnGeometry.getType(); - let center = ol.extent.getCenter(drawnGeometry.getExtent()); - let coordinates = drawnGeometry.getCoordinates(); - if (startingPoint) { - coordinates = concat(startingPoint, coordinates); - drawnGeometry.setCoordinates(coordinates); - } - if (type === "Circle") { - radius = Math.sqrt(Math.pow(center[0] - coordinates[0][0][0], 2) + Math.pow(center[1] - coordinates[0][0][1], 2)); + if (this.translateInteraction) { + this.props.map.removeInteraction(this.translateInteraction); } - let geometry = { - type, - extent: extent, - center: center, - coordinates: type === "Polygon" ? coordinates[0].concat([coordinates[0][0]]) : coordinates, - radius: radius, - projection: this.props.map.getView().getProjection().getCode() - }; - /*let modifyProps = assign({}, drawProps, { - features: features, - deleteCondition: () => false, - condition: ol.events.condition.never // TODO customize this part to edit + this.translateInteraction = new ol.interaction.Translate({ + features: this.selectInteraction.getFeatures() }); - let modify = new ol.interaction.Modify(modifyProps); - this.props.map.addInteraction(modify);*/ - this.props.onEndDrawing(geometry, this.props.drawOwner); - if (this.props.options.stopAfterDrawing) { - this.props.onChangeDrawingStatus('stop', this.props.drawMethod, this.props.drawOwner); + + this.translateInteraction.on('translateend', this.updateFeatureExtent); + this.props.map.addInteraction(this.translateInteraction); + + + if (this.modifyInteraction) { + this.props.map.removeInteraction(this.modifyInteraction); } - }, this); - this.props.map.addInteraction(draw); - this.drawInteraction = draw; + this.modifyInteraction = new ol.interaction.Modify({ + features: this.selectInteraction.getFeatures() + }); + + this.props.map.addInteraction(this.modifyInteraction); + } this.drawSource.clear(); if (newProps.features.length > 0 ) { this.addFeatures(newProps.features || []); } }, + addSelectInteraction() { + if (this.selectInteraction) { + this.props.map.removeInteraction(this.selectInteraction); + } + + this.selectInteraction = new ol.interaction.Select({ layers: [this.drawLayer] }); + + this.selectInteraction.on('select', () => { + let features = this.props.features.map(f => { + let selectedFeatures = this.selectInteraction.getFeatures().getArray(); + const selected = selectedFeatures.reduce((previous, current) => { + return current.get('id') === f.id ? true : previous; + }, false); + + return assign({}, f, { selected: selected }); + }); + + this.props.onChangeDrawingStatus('select', null, this.props.drawOwner, features); + }); + + this.props.map.addInteraction(this.selectInteraction); + }, removeDrawInteraction: function() { - if (this.drawInteraction !== null) { + if (this.drawInteraction) { this.props.map.removeInteraction(this.drawInteraction); this.drawInteraction = null; this.sketchFeature = null; + setTimeout(() => this.setDoubleClickZoomEnabled(true), 250); } }, - clean: function() { + removeInteractions: function() { this.removeDrawInteraction(); + if (this.selectInteraction) { + this.props.map.removeInteraction(this.drawInteraction); + } + + if (this.modifyInteraction) { + this.props.map.removeInteraction(this.modifyInteraction); + } + + if (this.translateInteraction) { + this.props.map.removeInteraction(this.translateInteraction); + } + }, + clean: function() { + this.removeInteractions(); + if (this.drawLayer) { this.props.map.removeLayer(this.drawLayer); this.geojson = null; this.drawLayer = null; this.drawSource = null; } + }, + fromOLFeature: function(feature, drawMethod, startingPoint) { + const geometry = feature.getGeometry(); + const extent = geometry.getExtent(); + const center = ol.extent.getCenter(geometry.getExtent()); + let coordinates = geometry.getCoordinates(); + let radius; + + const type = geometry.getType(); + if (startingPoint) { + coordinates = concat(startingPoint, coordinates); + geometry.setCoordinates(coordinates); + } + if (drawMethod === "Circle") { + radius = Math.sqrt(Math.pow(center[0] - coordinates[0][0][0], 2) + Math.pow(center[1] - coordinates[0][0][1], 2)); + } + + return { + id: feature.get('id'), + type: type, + extent: extent, + center: center, + coordinates, + radius: radius, + style: this.fromOlStyle(feature.getStyle()), + projection: this.props.map.getView().getProjection().getCode() + }; + }, + toOlFeature: function(feature) { + return head(this.drawSource.getFeatures().filter((f) => f.get('id') === feature.id)); + }, + fromOlStyle(olStyle) { + if (!olStyle) { + return {}; + } + + return { + fillColor: this.rgbToHex(olStyle.getFill().getColor()), + fillTransparency: olStyle.getFill().getColor()[3], + strokeColor: olStyle.getStroke().getColor(), + strokeWidth: olStyle.getStroke().getWidth(), + text: olStyle.getText().getText() + }; + }, + toOlStyle: function(style, selected) { + let color = style && style.fillColor ? style.fillColor : [255, 255, 255, 0.2]; + if (typeof color === 'string') { + color = this.hexToRgb(color); + } + + if (style && style.fillTransparency) { + color[3] = style.fillTransparency; + } + + let strokeColor = style && style.strokeColor ? style.strokeColor : '#ffcc33'; + if (selected) { + strokeColor = '#4a90e2'; + } + + return new ol.style.Style({ + fill: new ol.style.Fill({ + color: color + }), + stroke: new ol.style.Stroke({ + color: strokeColor, + width: style && style.strokeWidth ? style.strokeWidth : 2 + }), + image: new ol.style.Circle({ + radius: style && style.strokeWidth ? style.strokeWidth : 5, + fill: new ol.style.Fill({ color: style && style.strokeColor ? style.strokeColor : '#ffcc33' }) + }), + text: new ol.style.Text({ + text: style && style.text ? style.text : '', + fill: new ol.style.Fill({ color: style && style.strokeColor ? style.strokeColor : '#000' }), + stroke: new ol.style.Stroke({ color: '#fff', width: 2 }), + font: style && style.fontSize ? style.fontSize + 'px helvetica' : '' + }) + }); + }, + hexToRgb(hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.replace(shorthandRegex, function(m, r, g, b) { + return r + r + g + g + b + b; + })); + return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null; + }, + componentToHex(c) { + var hex = c.toString(16); + return hex.length === 1 ? "0" + hex : hex; + }, + rgbToHex(rgb) { + return "#" + this.componentToHex(rgb[0]) + this.componentToHex(rgb[1]) + this.componentToHex(rgb[2]); } }); diff --git a/web/client/components/map/openlayers/Locate.jsx b/web/client/components/map/openlayers/Locate.jsx index 4c281de585..97d4226a75 100644 --- a/web/client/components/map/openlayers/Locate.jsx +++ b/web/client/components/map/openlayers/Locate.jsx @@ -29,20 +29,15 @@ var Locate = React.createClass({ componentDidMount() { if (this.props.map) { this.locate = new OlLocate(this.props.map, this.defaultOpt); + this.locate.setStrings(this.props.messages); this.locate.options.onLocationError = this.onLocationError; this.locate.on("propertychange", (e) => {this.onStateChange(e.target.get(e.key)); }); + this.configureLocate(this.props.status); } }, componentWillReceiveProps(newProps) { - let state = this.locate.get("state"); if (newProps.status !== this.props.status) { - if ( newProps.status === "ENABLED" && state === "DISABLED") { - this.locate.start(); - }else if (newProps.status === "FOLLOWING" && state === "ENABLED") { - this.locate.startFollow(); - }else if (newProps.status === "DISABLED") { - this.locate.stop(); - } + this.configureLocate(newProps.status); } if (newProps.messages !== this.props.messages) { this.locate.setStrings(newProps.messages); @@ -60,6 +55,16 @@ var Locate = React.createClass({ render() { return null; }, + configureLocate(newStatus) { + let state = this.locate.get("state"); + if ( newStatus === "ENABLED" && state === "DISABLED") { + this.locate.start(); + } else if (newStatus === "FOLLOWING" && state === "ENABLED") { + this.locate.startFollow(); + } else if (newStatus === "DISABLED") { + this.locate.stop(); + } + }, defaultOpt: { follow: true,// follow with zoom and pan the user's location remainActive: true, diff --git a/web/client/components/map/openlayers/MeasurementSupport.jsx b/web/client/components/map/openlayers/MeasurementSupport.jsx index 082c879e02..599e686e72 100644 --- a/web/client/components/map/openlayers/MeasurementSupport.jsx +++ b/web/client/components/map/openlayers/MeasurementSupport.jsx @@ -159,7 +159,9 @@ const MeasurementSupport = React.createClass({ this.calculateGeodesicDistance(sketchCoords) : 0, area: this.props.measurement.geomType === 'Polygon' ? this.calculateGeodesicArea(this.sketchFeature.getGeometry().getLinearRing(0).getCoordinates()) : 0, - bearing: this.props.measurement.geomType === 'Bearing' ? bearing : 0 + bearing: this.props.measurement.geomType === 'Bearing' ? bearing : 0, + lenUnit: this.props.measurement.lenUnit, + areaUnit: this.props.measurement.areaUnit } ); this.props.changeMeasurementState(newMeasureState); diff --git a/web/client/components/map/openlayers/SelectionSupport.jsx b/web/client/components/map/openlayers/SelectionSupport.jsx new file mode 100644 index 0000000000..3f69b1a46a --- /dev/null +++ b/web/client/components/map/openlayers/SelectionSupport.jsx @@ -0,0 +1,145 @@ +/** + * Copyright 2017, Sourcepole AG. + * 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'); +var ol = require('openlayers'); + +const SelectionSupport = React.createClass({ + propTypes: { + map: React.PropTypes.object, + projection: React.PropTypes.string, + selection: React.PropTypes.object, + changeSelectionState: React.PropTypes.func + }, + getDefaultProps() { + return { + selection: {} + }; + }, + componentWillReceiveProps(newProps) { + if (newProps.selection.geomType && newProps.selection.geomType !== this.props.selection.geomType ) { + this.addDrawInteraction(newProps); + } + + if (!newProps.selection.geomType) { + this.removeDrawInteraction(); + } + }, + render() { + return null; + }, + addDrawInteraction: function(newProps) { + // cleanup old interaction + if (this.drawInteraction) { + this.removeDrawInteraction(); + } + // create a layer to draw on + let source = new ol.source.Vector(); + let vector = new ol.layer.Vector({ + source: source, + zIndex: 1000000, + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#ffcc33', + width: 2 + }), + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: '#ffcc33' + }) + }) + }) + }); + + this.props.map.addLayer(vector); + + // create an interaction to draw with + let draw = new ol.interaction.Draw({ + source: source, + type: /** @type {ol.geom.GeometryType} */ newProps.selection.geomType, + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 0, 0.5)', + lineDash: [10, 10], + width: 2 + }), + image: new ol.style.Circle({ + radius: 5, + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }) + }) + }) + }); + + draw.on('drawstart', function(evt) { + // preserv the sketch feature of the draw controller + // to update length/area on drawing a new vertex + this.sketchFeature = evt.feature; + // clear previous sketches + source.clear(); + }, this); + draw.on('drawend', function() { + this.updateSelectionState(); + }, this); + + this.props.map.addInteraction(draw); + this.drawInteraction = draw; + this.selectionLayer = vector; + this.setDoubleClickZoomEnabled(false); + }, + removeDrawInteraction: function() { + if (this.drawInteraction !== null) { + this.props.map.removeInteraction(this.drawInteraction); + this.drawInteraction = null; + this.props.map.removeLayer(this.selectionLayer); + this.sketchFeature = null; + // Delay execution of activation of double click zoom function + setTimeout(() => this.setDoubleClickZoomEnabled(true), 251); + } + }, + updateSelectionState() { + if (!this.sketchFeature) { + return; + } + const sketchCoords = this.sketchFeature.getGeometry().getCoordinates(); + + let newSelectionState = { + geomType: this.props.selection.geomType, + point: this.props.selection.geomType === 'Point' ? + [sketchCoords[0], sketchCoords[1]] : null, + line: this.props.selection.geomType === 'LineString' ? + sketchCoords.map(coo => [coo[0], coo[1]]) : null, + polygon: this.props.selection.geomType === 'Polygon' ? + this.sketchFeature.getGeometry().getLinearRing(0).getCoordinates().map(coo => [coo[0], coo[1]]) : null + }; + this.props.changeSelectionState(newSelectionState); + }, + setDoubleClickZoomEnabled(enabled) { + let interactions = this.props.map.getInteractions(); + for (let i = 0; i < interactions.getLength(); i++) { + let interaction = interactions.item(i); + if (interaction instanceof ol.interaction.DoubleClickZoom) { + interaction.setActive(enabled); + break; + } + } + } +}); + +module.exports = SelectionSupport; diff --git a/web/client/components/map/openlayers/plugins/GoogleLayer.js b/web/client/components/map/openlayers/plugins/GoogleLayer.js index d5d3beab94..c2bf636634 100644 --- a/web/client/components/map/openlayers/plugins/GoogleLayer.js +++ b/web/client/components/map/openlayers/plugins/GoogleLayer.js @@ -40,17 +40,16 @@ Layers.registerType('google', { }); } gmaps[mapId].setMapTypeId(layersMap[options.name]); - let view = map.getView(); let mapContainer = document.getElementById(mapId + 'gmaps'); let setCenter = function() { if (mapContainer.style.visibility !== 'hidden') { - const center = ol.proj.transform(view.getCenter(), 'EPSG:3857', 'EPSG:4326'); + const center = ol.proj.transform(map.getView().getCenter(), 'EPSG:3857', 'EPSG:4326'); gmaps[mapId].setCenter(new google.maps.LatLng(center[1], center[0])); } }; let setZoom = function() { if (mapContainer.style.visibility !== 'hidden') { - gmaps[mapId].setZoom(view.getZoom()); + gmaps[mapId].setZoom(map.getView().getZoom()); } }; @@ -103,18 +102,22 @@ Layers.registerType('google', { let setRotation = function() { if (mapContainer.style.visibility !== 'hidden') { - const rotation = view.getRotation() * 180 / Math.PI; + const rotation = map.getView().getRotation() * 180 / Math.PI; mapContainer.style.transform = "rotate(" + rotation + "deg)"; google.maps.event.trigger(gmaps[mapId], "resize"); } }; - view.on('change:center', setCenter); - view.on('change:resolution', setZoom); - view.on('change:rotation', setRotation); - + let setViewEventListeners = function() { + let view = map.getView(); + view.on('change:center', setCenter); + view.on('change:resolution', setZoom); + view.on('change:rotation', setRotation); + }; + map.on('change:view', setViewEventListeners); + setViewEventListeners(); setCenter(); setZoom(); diff --git a/web/client/components/map/openlayers/plugins/VectorLayer.js b/web/client/components/map/openlayers/plugins/VectorLayer.js index 803021f372..6f5ea51532 100644 --- a/web/client/components/map/openlayers/plugins/VectorLayer.js +++ b/web/client/components/map/openlayers/plugins/VectorLayer.js @@ -21,25 +21,25 @@ const image = new ol.style.Circle({ }); const defaultStyles = { - 'Point': [new ol.style.Style({ + 'Point': () => [new ol.style.Style({ image: image })], - 'LineString': [new ol.style.Style({ + 'LineString': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'green', width: 1 }) })], - 'MultiLineString': [new ol.style.Style({ + 'MultiLineString': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'green', width: 1 }) })], - 'MultiPoint': [new ol.style.Style({ + 'MultiPoint': () => [new ol.style.Style({ image: image })], - 'MultiPolygon': [new ol.style.Style({ + 'MultiPolygon': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'blue', lineDash: [4], @@ -49,7 +49,7 @@ const defaultStyles = { color: 'rgba(0, 0, 255, 0.1)' }) })], - 'Polygon': [new ol.style.Style({ + 'Polygon': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'blue', lineDash: [4], @@ -59,7 +59,7 @@ const defaultStyles = { color: 'rgba(0, 0, 255, 0.1)' }) })], - 'GeometryCollection': [new ol.style.Style({ + 'GeometryCollection': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'magenta', width: 2 @@ -75,7 +75,7 @@ const defaultStyles = { }) }) })], - 'Circle': [new ol.style.Style({ + 'Circle': () => [new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'red', width: 2 @@ -84,25 +84,32 @@ const defaultStyles = { color: 'rgba(255,0,0,0.2)' }) })], - 'marker': [new ol.style.Style({ - image: new ol.style.Icon(({ + 'marker': (options) => [new ol.style.Style({ + image: new ol.style.Icon({ anchor: [14, 41], anchorXUnits: 'pixels', anchorYUnits: 'pixels', src: markerShadow - })) + }) }), new ol.style.Style({ - image: new ol.style.Icon(({ + image: new ol.style.Icon({ anchor: [0.5, 1], anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: markerIcon - })) + }), + text: new ol.style.Text({ + text: options.label, + scale: 1.25, + offsetY: 8, + fill: new ol.style.Fill({color: '#000000'}), + stroke: new ol.style.Stroke({color: '#FFFFFF', width: 2}) + }) })] }; -var styleFunction = function(feature) { - return defaultStyles[feature.getGeometry().getType()]; +var styleFunction = function(feature, options) { + return defaultStyles[feature.getGeometry().getType()](options); }; function getStyle(options) { @@ -163,12 +170,12 @@ function getStyle(options) { switch (type) { case "Point": case "MultiPoint": - return defaultStyles.marker; + return defaultStyles.marker(options); default: break; } } - return defaultStyles[options.styleName]; + return defaultStyles[options.styleName](options); } : style || styleFunction; } Layers.registerType('vector', { @@ -197,6 +204,9 @@ Layers.registerType('vector', { f.getGeometry().transform(oldCrs, newCrs); }); } + if (!newOptions.overrideOLStyle) { + layer.setStyle((feature) => styleFunction(feature, newOptions)); + } if (!isEqual(oldOptions.style, newOptions.style)) { layer.setStyle(getStyle(newOptions)); } diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index f1aa5fcb74..1c40e7bdb0 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -55,7 +55,8 @@ Layers.registerType('wms', { zIndex: options.zIndex, source: new ol.source.ImageWMS({ url: urls[0], - params: queryParameters + params: queryParameters, + ratio: options.ratio }) }); } diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js index 3812af626f..b6ba3844ab 100644 --- a/web/client/components/map/openlayers/plugins/WMTSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -44,7 +44,8 @@ Layers.registerType('wmts', { ], extent: extent, resolutions: resolutions, - matrixIds: matrixIds + matrixIds: matrixIds, + tileSize: options.tileSize || [256, 256] }), style: options.style || '', wrapX: true diff --git a/web/client/plugins/map/index.js b/web/client/plugins/map/index.js index f904d18680..1d5e5ab865 100644 --- a/web/client/plugins/map/index.js +++ b/web/client/plugins/map/index.js @@ -12,8 +12,9 @@ const {changeMapView, clickOnMap} = require('../../actions/map'); const {layerLoading, layerLoad, layerError, invalidLayer} = require('../../actions/layers'); const {changeMousePosition} = require('../../actions/mousePosition'); const {changeMeasurementState} = require('../../actions/measurement'); +const {changeSelectionState} = require('../../actions/selection'); const {changeLocateState, onLocateError} = require('../../actions/locate'); -const {changeDrawingStatus, endDrawing} = require('../../actions/draw'); +const {changeDrawingStatus, endDrawing, setCurrentStyle} = require('../../actions/draw'); const {updateHighlighted} = require('../../actions/highlight'); const {connect} = require('react-redux'); @@ -48,7 +49,8 @@ module.exports = (mapType, actions) => { })(components.MeasurementSupport || Empty); const Locate = connect((state) => ({ - status: state.locate && state.locate.state + status: state.locate && state.locate.state, + messages: state.locale && state.locale.messages ? state.locale.messages.locate : undefined }), { changeLocateState, onLocateError @@ -57,12 +59,19 @@ module.exports = (mapType, actions) => { const DrawSupport = connect((state) => ( state.draw || {}), { onChangeDrawingStatus: changeDrawingStatus, - onEndDrawing: endDrawing + onEndDrawing: endDrawing, + setCurrentStyle: setCurrentStyle })( components.DrawSupport || Empty); const HighlightSupport = connect((state) => ( state.highlight || {}), {updateHighlighted})( components.HighlightFeatureSupport || Empty); + const SelectionSupport = connect((state) => ({ + selection: state.selection || {} + }), { + changeSelectionState + })(components.SelectionSupport || Empty); + require('../../components/map/' + mapType + '/plugins/index'); return { @@ -75,7 +84,8 @@ module.exports = (mapType, actions) => { overview: components.Overview || Empty, scalebar: components.ScaleBar || Empty, draw: DrawSupport, - highlight: HighlightSupport + highlight: HighlightSupport, + selection: SelectionSupport } }; }; diff --git a/web/client/plugins/map/openlayers/index.js b/web/client/plugins/map/openlayers/index.js index 7307198218..ef46b01af8 100644 --- a/web/client/plugins/map/openlayers/index.js +++ b/web/client/plugins/map/openlayers/index.js @@ -15,5 +15,6 @@ module.exports = { Overview: require('../../../components/map/openlayers/Overview'), ScaleBar: require('../../../components/map/openlayers/ScaleBar'), DrawSupport: require('../../../components/map/openlayers/DrawSupport'), - HighlightFeatureSupport: require('../../../components/map/openlayers/HighlightFeatureSupport') + HighlightFeatureSupport: require('../../../components/map/openlayers/HighlightFeatureSupport'), + SelectionSupport: require('../../../components/map/openlayers/SelectionSupport') }; diff --git a/web/client/reducers/draw.js b/web/client/reducers/draw.js index 293e036056..51442af2b2 100644 --- a/web/client/reducers/draw.js +++ b/web/client/reducers/draw.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -const {CHANGE_DRAWING_STATUS} = require('../actions/draw'); +const {CHANGE_DRAWING_STATUS, SET_CURRENT_STYLE} = require('../actions/draw'); const assign = require('object-assign'); @@ -28,6 +28,10 @@ function draw(state = initialState, action) { options: action.options, features: action.features }); + case SET_CURRENT_STYLE: + return assign({}, state, { + currentStyle: action.currentStyle + }); default: return state; } diff --git a/web/client/reducers/map.js b/web/client/reducers/map.js index 99dbaf836c..35098957ff 100644 --- a/web/client/reducers/map.js +++ b/web/client/reducers/map.js @@ -8,7 +8,7 @@ var {CHANGE_MAP_VIEW, CHANGE_MOUSE_POINTER, CHANGE_ZOOM_LVL, CHANGE_MAP_CRS, CHANGE_MAP_SCALES, ZOOM_TO_EXTENT, PAN_TO, - CHANGE_MAP_STYLE, CHANGE_ROTATION} = require('../actions/map'); + CHANGE_MAP_STYLE, CHANGE_ROTATION, ZOOM_TO_POINT} = require('../actions/map'); const {isArray} = require('lodash'); @@ -43,7 +43,8 @@ function mapConfig(state = null, action) { mapOptions: assign({}, state && state.mapOptions, { view: assign({}, state && state.mapOptions && state.mapOptions.view, { - resolutions: resolutions + resolutions: resolutions, + scales: action.scales }) }) }); @@ -109,6 +110,13 @@ function mapConfig(state = null, action) { } return state; } + case ZOOM_TO_POINT: { + return assign({}, state, { + center: CoordinatesUtils.reproject(action.pos, action.crs, 'EPSG:4326'), + zoom: action.zoom, + mapStateSource: null + }); + } case PAN_TO: { const center = CoordinatesUtils.reproject( action.center, diff --git a/web/client/reducers/search.js b/web/client/reducers/search.js index 1b146b1643..0bcafe30f5 100644 --- a/web/client/reducers/search.js +++ b/web/client/reducers/search.js @@ -7,7 +7,7 @@ */ var {TEXT_SEARCH_RESULTS_LOADED, TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, TEXT_SEARCH_ADD_MARKER, TEXT_SEARCH_TEXT_CHANGE, TEXT_SEARCH_LOADING, TEXT_SEARCH_ERROR, - TEXT_SEARCH_NESTED_SERVICES_SELECTED, TEXT_SEARCH_CANCEL_ITEM} = require('../actions/search'); + TEXT_SEARCH_NESTED_SERVICES_SELECTED, TEXT_SEARCH_CANCEL_ITEM, TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE} = require('../actions/search'); var {RESET_CONTROLS} = require('../actions/controls'); const assign = require('object-assign'); @@ -90,7 +90,9 @@ function search(state = null, action) { case TEXT_SEARCH_RESULTS_PURGE: return assign({}, state, { results: null, error: null}); case TEXT_SEARCH_ADD_MARKER: - return assign({}, state, { markerPosition: action.markerPosition }); + return assign({}, state, { markerPosition: action.markerPosition, markerLabel: action.markerLabel }); + case TEXT_SEARCH_SET_HIGHLIGHTED_FEATURE: + return assign({}, state, {highlightedFeature: action.highlightedFeature}); case TEXT_SEARCH_RESET: case RESET_CONTROLS: return null; diff --git a/web/client/reducers/selection.js b/web/client/reducers/selection.js new file mode 100644 index 0000000000..c045b4f674 --- /dev/null +++ b/web/client/reducers/selection.js @@ -0,0 +1,31 @@ +/** + * Copyright 2017, Sourcepole AG. + * 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. + */ + +var { + CHANGE_SELECTION_STATE +} = require('../actions/selection'); + +const assign = require('object-assign'); + +function selection(state = { + geomType: null +}, action) { + switch (action.type) { + case CHANGE_SELECTION_STATE: + return assign({}, state, { + geomType: action.geomType, + point: action.point, + line: action.line, + polygon: action.polygon + }); + default: + return state; + } +} + +module.exports = selection; diff --git a/web/client/selectors/layers.js b/web/client/selectors/layers.js index c714ed941b..300e51cd24 100644 --- a/web/client/selectors/layers.js +++ b/web/client/selectors/layers.js @@ -13,20 +13,20 @@ const LayersUtils = require('../utils/LayersUtils'); const layersSelector = state => (state.layers && state.layers.flat) || (state.layers) || (state.config && state.config.layers); const markerSelector = state => (state.mapInfo && state.mapInfo.showMarker && state.mapInfo.clickPoint); -const geoColderSelector = state => (state.search && state.search.markerPosition); +const geoColderSelector = state => (state.search && state.search); // TODO currently loading flag causes a re-creation of the selector on any pan // to avoid this separate loading from the layer object const layerSelectorWithMarkers = createSelector( [layersSelector, markerSelector, geoColderSelector], - (layers = [], markerPosition, geocoderPosition) => { + (layers = [], markerPosition, geocoder) => { let newLayers = [...layers]; if ( markerPosition ) { newLayers.push(MapInfoUtils.getMarkerLayer("GetFeatureInfo", markerPosition.latlng)); } - if (geocoderPosition) { - newLayers.push(MapInfoUtils.getMarkerLayer("GeoCoder", geocoderPosition, "marker", + if (geocoder && geocoder.markerPosition) { + newLayers.push(MapInfoUtils.getMarkerLayer("GeoCoder", geocoder.markerPosition, "marker", { overrideOLStyle: true, style: { @@ -37,7 +37,7 @@ const layerSelectorWithMarkers = createSelector( popupAnchor: [1, -34], shadowSize: [41, 41] } - } + }, geocoder.markerLabel )); } diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index d25fe0e347..60a331d633 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -49,11 +49,20 @@ function determineCrs(crs) { } return crs; } + +let crsLabels = { + "EPSG:4326": "WGS 84", + "EPSG:3857": "WGS 84 / Pseudo Mercator" +}; + /** * Utilities for Coordinates conversion. * @memberof utils */ const CoordinatesUtils = { + setCrsLabels(labels) { + crsLabels = assign({}, crsLabels, labels); + }, getUnits: function(projection) { const proj = new Proj4js.Proj(projection); return proj.units || 'degrees'; @@ -242,7 +251,7 @@ const CoordinatesUtils = { let crsList = {}; for (let a in Proj4js.defs) { if (Proj4js.defs.hasOwnProperty(a)) { - crsList[a] = {label: a}; + crsList[a] = {label: crsLabels[a] || a}; } } return crsList; diff --git a/web/client/utils/FilterUtils.jsx b/web/client/utils/FilterUtils.jsx index 9dac1ff7f0..bf4cfd00f6 100644 --- a/web/client/utils/FilterUtils.jsx +++ b/web/client/utils/FilterUtils.jsx @@ -359,6 +359,16 @@ const FilterUtils = { } return filter; }, + closePolygon: function(coords) { + if (coords.length >= 3) { + const first = coords[0]; + const last = coords[coords.length - 1]; + if ((first[0] !== last[0]) || (first[1] !== last[1])) { + return coords.concat([coords[0]]); + } + } + return coords; + }, getGmlPolygonElement: function(coordinates, srsName, version) { let gmlPolygon = ' { - let coords = element.map((coordinate) => { + let coords = this.closePolygon(element).map((coordinate) => { return coordinate[0] + (version === "1.0.0" ? "," : " ") + coordinate[1]; }); const exterior = (version === "1.0.0" ? "outerBoundaryIs" : "exterior"); diff --git a/web/client/utils/MapInfoUtils.js b/web/client/utils/MapInfoUtils.js index 0bf998f6ac..ac771fb8f4 100644 --- a/web/client/utils/MapInfoUtils.js +++ b/web/client/utils/MapInfoUtils.js @@ -76,12 +76,13 @@ const MapInfoUtils = { } ]; }, - getMarkerLayer(name, clickedMapPoint, styleName, otherParams) { + getMarkerLayer(name, clickedMapPoint, styleName, otherParams, markerLabel) { return { type: 'vector', visibility: true, name: name || "GetFeatureInfo", styleName: styleName || "marker", + label: markerLabel, features: MapInfoUtils.clickedPointToGeoJson(clickedMapPoint), ...otherParams }; diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index f0d3d716f8..cac89587cf 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -266,6 +266,22 @@ function mapUpdated(oldMap, newMap) { return !centersEqual || (newMap.zoom !== oldMap.zoom); } +/* Transform width and height specified in meters to the units of the specified projection */ +function transformExtent(projection, center, width, height) { + let units = CoordinatesUtils.getUnits(projection); + if (units === 'ft') { + return {width: width / METERS_PER_UNIT.ft, height: height / METERS_PER_UNIT.ft}; + } else if (units === 'us-ft') { + return {width: width / METERS_PER_UNIT['us-ft'], height: height / METERS_PER_UNIT['us-ft']}; + } else if (units === 'degrees') { + return { + width: width / (111132.92 - 559.82 * Math.cos(2 * center.y) + 1.175 * Math.cos(4 * center.y)), + height: height / (111412.84 * Math.cos(center.y) - 93.5 * Math.cos(3 * center.y)) + }; + } + return {width, height}; +} + module.exports = { EXTENT_TO_ZOOM_HOOK, RESOLUTIONS_HOOK, @@ -290,5 +306,6 @@ module.exports = { getScales, getBbox, mapUpdated, - getCurrentResolution + getCurrentResolution, + transformExtent };