diff --git a/web/client/components/map/leaflet/Feature.jsx b/web/client/components/map/leaflet/Feature.jsx
index 17f2d21e23..678cd9efc1 100644
--- a/web/client/components/map/leaflet/Feature.jsx
+++ b/web/client/components/map/leaflet/Feature.jsx
@@ -107,7 +107,7 @@ var geometryToLayer = function(geojson, options) {
return new L.FeatureGroup(layers);
case 'GeometryCollection':
for (i = 0, len = geometry.geometries.length; i < len; i++) {
- layer = this.geometryToLayer({
+ layer = geometryToLayer({
geometry: geometry.geometries[i],
type: 'Feature',
properties: geojson.properties
diff --git a/web/client/components/map/leaflet/Layer.jsx b/web/client/components/map/leaflet/Layer.jsx
index 878406d19c..38c561a39f 100644
--- a/web/client/components/map/leaflet/Layer.jsx
+++ b/web/client/components/map/leaflet/Layer.jsx
@@ -8,6 +8,7 @@
var React = require('react');
var Layers = require('../../../utils/leaflet/Layers');
var assign = require('object-assign');
+var {isEqual} = require('lodash');
const LeafletLayer = React.createClass({
propTypes: {
@@ -45,6 +46,26 @@ const LeafletLayer = React.createClass({
}
this.updateLayer(newProps, this.props);
},
+ shouldComponentUpdate(newProps) {
+ // the reduce returns true when a prop is changed
+ // optimizing when options are equal ignorning loading key
+ return !(["map", "type", "srs", "position", "zoomOffset", "onInvalid", "onClick", "options"].reduce( (prev, p) => {
+ switch (p) {
+ case "map":
+ case "type":
+ case "srs":
+ case "position":
+ case "zoomOffset":
+ case "onInvalid":
+ case "onClick":
+ return prev && this.props[p] === newProps[p];
+ case "options":
+ return prev && (this.props[p] === newProps[p] || isEqual({...this.props[p], loading: false}, {...newProps[p], loading: false}));
+ default:
+ return prev;
+ }
+ }, true));
+ },
componentWillUnmount() {
if (this.layer && this.props.map) {
this.removeLayer();
@@ -105,6 +126,7 @@ const LeafletLayer = React.createClass({
this.layer.layerName = options.name;
this.layer.layerId = options.id;
}
+ this.forceUpdate();
}
},
updateLayer(newProps, oldProps) {
diff --git a/web/client/components/map/leaflet/__tests__/Layer-test.jsx b/web/client/components/map/leaflet/__tests__/Layer-test.jsx
index 620ba1d2d2..25e75909f2 100644
--- a/web/client/components/map/leaflet/__tests__/Layer-test.jsx
+++ b/web/client/components/map/leaflet/__tests__/Layer-test.jsx
@@ -9,6 +9,7 @@ var React = require('react/addons');
var ReactDOM = require('react-dom');
var L = require('leaflet');
var LeafLetLayer = require('../Layer.jsx');
+var Feature = require('../Feature.jsx');
var expect = require('expect');
require('../../../../utils/leaflet/Layers');
@@ -18,6 +19,7 @@ require('../plugins/WMSLayer');
require('../plugins/GoogleLayer');
require('../plugins/BingLayer');
require('../plugins/MapQuest');
+require('../plugins/VectorLayer');
describe('Leaflet layer', () => {
let map;
@@ -177,6 +179,119 @@ describe('Leaflet layer', () => {
expect(urls.length).toBe(1);
});
+ it('creates a vector layer for leaflet map', () => {
+ var options = {
+ "type": "wms",
+ "visibility": true,
+ "name": "vector_sample",
+ "group": "sample",
+ "features": [
+ { "type": "Feature",
+ "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
+ "properties": {"prop0": "value0"}
+ },
+ { "type": "Feature",
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": 0.0
+ }
+ },
+ { "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
+ [100.0, 1.0], [100.0, 0.0] ]
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ },
+ { "type": "Feature",
+ "geometry": { "type": "MultiPoint",
+ "coordinates": [ [100.0, 0.0], [101.0, 1.0] ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ },
+ { "type": "Feature",
+ "geometry": { "type": "MultiLineString",
+ "coordinates": [
+ [ [100.0, 0.0], [101.0, 1.0] ],
+ [ [102.0, 2.0], [103.0, 3.0] ]
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ },
+ { "type": "Feature",
+ "geometry": { "type": "MultiPolygon",
+ "coordinates": [
+ [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
+ [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
+ [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ },
+ { "type": "Feature",
+ "geometry": { "type": "GeometryCollection",
+ "geometries": [
+ { "type": "Point",
+ "coordinates": [100.0, 0.0]
+ },
+ { "type": "LineString",
+ "coordinates": [ [101.0, 0.0], [102.0, 1.0] ]
+ }
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ }
+ ]
+ };
+ // create layers
+ var layer = ReactDOM.render(
+ (
+ {options.features.map((feature) => ())}), document.getElementById("container"));
+ expect(layer).toExist();
+ let l2 = ReactDOM.render(
+ (
+ {options.features.map((feature) => ())}), document.getElementById("container"));
+ expect(l2).toExist();
+ });
+
it('creates a wms layer for leaflet map with custom tileSize', () => {
var options = {
"type": "wms",
diff --git a/web/client/components/map/openlayers/Layer.jsx b/web/client/components/map/openlayers/Layer.jsx
index 63a1d55ed5..e4e74f5e5b 100644
--- a/web/client/components/map/openlayers/Layer.jsx
+++ b/web/client/components/map/openlayers/Layer.jsx
@@ -104,6 +104,7 @@ const OpenlayersLayer = React.createClass({
if (this.layer && !this.layer.detached) {
this.addLayer(options);
}
+ this.forceUpdate();
}
},
updateLayer(newProps, oldProps) {
diff --git a/web/client/components/print/MapPreview.jsx b/web/client/components/print/MapPreview.jsx
index 13e95d6dc4..abf505dddf 100644
--- a/web/client/components/print/MapPreview.jsx
+++ b/web/client/components/print/MapPreview.jsx
@@ -14,6 +14,7 @@ const {Button, Glyphicon} = require('react-bootstrap');
let PMap;
let Layer;
+let Feature;
const MapPreview = React.createClass({
propTypes: {
@@ -54,6 +55,7 @@ const MapPreview = React.createClass({
PMap = require('../map/' + this.props.mapType + '/Map');
Layer = require('../map/' + this.props.mapType + '/Layer');
require('../map/' + this.props.mapType + '/plugins/index');
+ Feature = require('../map/' + this.props.mapType + '/index').Feature;
},
getRatio() {
if (this.props.width && this.props.layoutSize && this.props.resolutions) {
@@ -77,6 +79,22 @@ const MapPreview = React.createClass({
})
});
},
+ renderLayerContent(layer) {
+ if (layer.features && layer.type === "vector") {
+ return layer.features.map( (feature) => {
+ return (
+
+ );
+ });
+ }
+ return null;
+ },
render() {
const style = assign({}, this.props.style, {
width: this.props.width + "px",
@@ -102,8 +120,11 @@ const MapPreview = React.createClass({
mapOptions={mapOptions}
>
{this.props.layers.map((layer, index) =>
-
+
+ {this.renderLayerContent(layer)}
+
+
)}
{this.props.enableScalebox ? = 2 &&
+ typeof list[0] === 'number' &&
+ typeof list[1] === 'number';
+}
+function traverseCoords(coordinates, callback) {
+ if (isXY(coordinates)) return callback(coordinates);
+ return coordinates.map(function(coord) { return traverseCoords(coord, callback); });
+}
-var CoordinatesUtils = {
+function traverseGeoJson(geojson, leafCallback, nodeCallback) {
+ if (geojson === null) return geojson;
+
+ let r = cloneDeep(geojson);
+
+ if (geojson.type === 'Feature') {
+ r.geometry = traverseGeoJson(geojson.geometry, leafCallback, nodeCallback);
+ } else if (geojson.type === 'FeatureCollection') {
+ r.features = r.features.map(function(gj) { return traverseGeoJson(gj, leafCallback, nodeCallback); });
+ } else if (geojson.type === 'GeometryCollection') {
+ r.geometries = r.geometries.map(function(gj) { return traverseGeoJson(gj, leafCallback, nodeCallback); });
+ } else {
+ if (leafCallback) leafCallback(r);
+ }
+
+ if (nodeCallback) nodeCallback(r);
+
+ return r;
+}
+
+function determineCrs(crs) {
+ if (typeof crs === 'string' || crs instanceof String) {
+ return Proj4js.defs(crs) ? new Proj4js.Proj(crs) : null;
+ }
+ return crs;
+}
+
+const CoordinatesUtils = {
getUnits: function(projection) {
const proj = new Proj4js.Proj(projection);
return proj.units || 'degrees';
@@ -27,6 +65,52 @@ var CoordinatesUtils = {
}
return null;
},
+ /**
+ * Reprojects a geojson from a crs into another
+ */
+ reprojectGeoJson: function(geojson, fromParam = "EPSG:4326", toParam = "EPSG:4326") {
+ let from = fromParam;
+ let to = toParam;
+ if (typeof from === 'string') {
+ from = determineCrs(from);
+ }
+ if (typeof to === 'string') {
+ to = determineCrs(to);
+ }
+ let transform = proj4(from, to);
+
+ return traverseGeoJson(geojson, (gj) => {
+ // No easy way to put correct CRS info into the GeoJSON,
+ // and definitely wrong to keep the old, so delete it.
+ if (gj.crs) {
+ delete gj.crs;
+ }
+ gj.coordinates = traverseCoords(gj.coordinates, (xy) => {
+ return transform.forward(xy);
+ });
+ }, (gj) => {
+ if (gj.bbox) {
+ // A bbox can't easily be reprojected, just reprojecting
+ // the min/max coords definitely will not work since
+ // the transform is not linear (in the general case).
+ // Workaround is to just re-compute the bbox after the
+ // transform.
+ gj.bbox = (() => {
+ let min = [Number.MAX_VALUE, Number.MAX_VALUE];
+ let max = [-Number.MAX_VALUE, -Number.MAX_VALUE];
+ traverseGeoJson(gj, function(_gj) {
+ traverseCoords(_gj.coordinates, function(xy) {
+ min[0] = Math.min(min[0], xy[0]);
+ min[1] = Math.min(min[1], xy[1]);
+ max[0] = Math.max(max[0], xy[0]);
+ max[1] = Math.max(max[1], xy[1]);
+ });
+ });
+ return [min[0], min[1], max[0], max[1]];
+ })();
+ }
+ });
+ },
normalizePoint: function(point) {
return {
x: point.x || 0.0,
diff --git a/web/client/utils/PrintUtils.js b/web/client/utils/PrintUtils.js
index 0397ded09b..9aa9ab4be1 100644
--- a/web/client/utils/PrintUtils.js
+++ b/web/client/utils/PrintUtils.js
@@ -9,6 +9,7 @@
const CoordinatesUtils = require('./CoordinatesUtils');
const MapUtils = require('./MapUtils');
+
const {isArray} = require('lodash');
const url = require('url');
@@ -17,6 +18,10 @@ const defaultScales = MapUtils.getGoogleMercatorScales(0, 21);
const assign = require('object-assign');
+const getGeomType = function(layer) {
+ return (layer.features && layer.features[0]) ? layer.features[0].geometry.type : undefined;
+};
+
const PrintUtils = {
normalizeUrl: (input) => {
let result = isArray(input) ? input[0] : input;
@@ -140,6 +145,23 @@ const PrintUtils = {
]
})
},
+ vector: {
+ map: (layer, spec) => ({
+ type: 'Vector',
+ name: layer.name,
+ "opacity": layer.opacity || 1.0,
+ styleProperty: "ms_style",
+ styles: {
+ 1: PrintUtils.toOpenLayers2Style(layer, layer.style)
+ },
+ geoJson: CoordinatesUtils.reprojectGeoJson({
+ type: "FeatureCollection",
+ features: layer.features.map( f => ({...f, properties: {...f.properties, ms_style: 1}}))
+ },
+ "EPSG:4326",
+ spec.projection)
+ })
+ },
osm: {
map: () => ({
"baseURL": "http://a.tile.openstreetmap.org/",
@@ -220,6 +242,94 @@ const PrintUtils = {
]
})
}
+ },
+ /**
+ * Useful for print (Or generic Openlayers 2 conversion style)
+ */
+ toOpenLayers2Style: function(layer, style) {
+ if (!style) {
+ return PrintUtils.getOlDefaultStyle(layer);
+ }
+ // commented the available options.
+ return {
+ "fillColor": style.fillColor,
+ "fillOpacity": style.fillOpacity,
+ // "rotation": "30",
+ "externalGraphic": style.iconUrl,
+ // "graphicName": "circle",
+ // "graphicOpacity": 0.4,
+ "pointRadius": style.radius,
+ "strokeColor": style.color,
+ "strokeOpacity": style.opacity,
+ "strokeWidth": style.weight
+ // "strokeLinecap": "round",
+ // "strokeDashstyle": "dot",
+ // "fontColor": "#000000",
+ // "fontFamily": "sans-serif",
+ // "fontSize": "12px",
+ // "fontStyle": "normal",
+ // "fontWeight": "bold",
+ // "haloColor": "#123456",
+ // "haloOpacity": "0.7",
+ // "haloRadius": "3.0",
+ // "label": "${name}",
+ // "labelAlign": "cm",
+ // "labelRotation": "45",
+ // "labelXOffset": "-25.0",
+ // "labelYOffset": "-35.0"
+ };
+ },
+ /**
+ * Provides the default style for
+ * each vector type.
+ */
+ getOlDefaultStyle(layer) {
+ switch (getGeomType(layer)) {
+ case 'Polygon':
+ case 'MultiPolygon': {
+ return {
+ "fillColor": "#0000FF",
+ "fillOpacity": 0.1,
+ "strokeColor": "#0000FF",
+ "strokeOpacity": 1,
+ "strokeWidth": 3
+ };
+ }
+ case 'MultiLineString':
+ case 'LineString':
+ return {
+ "strokeColor": "#0000FF",
+ "strokeOpacity": 1,
+ "strokeWidth": 3
+ };
+ case 'Point':
+ case 'MultiPoint': {
+ return layer.styleName === "marker" ? {
+ "externalGraphic": "http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/images/marker-icon.png",
+ "graphicWidth": 25,
+ "graphicHeight": 41,
+ "graphicXOffset": -12, // different offset
+ "graphicYOffset": -41
+ } : {
+ "fillColor": "#FF0000",
+ "fillOpacity": 0,
+ "strokeColor": "#FF0000",
+ "pointRadius": 5,
+ "strokeOpacity": 1,
+ "strokeWidth": 1
+ };
+ }
+ default: {
+ return {
+ "fillColor": "#0000FF",
+ "fillOpacity": 0.1,
+ "strokeColor": "#0000FF",
+ "pointRadius": 5,
+ "strokeOpacity": 1,
+ "strokeWidth": 1
+ };
+ }
+ }
}
};
diff --git a/web/client/utils/__tests__/CoordinatesUtils-test.js b/web/client/utils/__tests__/CoordinatesUtils-test.js
index 3547e33946..c007b8e0fe 100644
--- a/web/client/utils/__tests__/CoordinatesUtils-test.js
+++ b/web/client/utils/__tests__/CoordinatesUtils-test.js
@@ -72,4 +72,34 @@ describe('CoordinatesUtils', () => {
expect(CoordinatesUtils.getCompatibleSRS('EPSG:3857', {'EPSG:900913': true, 'EPSG:3857': true})).toBe('EPSG:3857');
expect(CoordinatesUtils.getCompatibleSRS('EPSG:3857', {'EPSG:3857': true})).toBe('EPSG:3857');
});
+ it('test reprojectGeoJson', () => {
+ const testPoint = {
+ type: "FeatureCollection",
+ features: [
+ {
+ type: "Feature",
+ geometry: {
+ type: "Point",
+ coordinates: [
+ -112.50042920000001,
+ 42.22829164089942
+ ]
+ },
+ properties: {
+ "serial_num": "12C324776"
+ },
+ id: 0
+ }
+ ]
+ };
+ const reprojectedTestPoint = CoordinatesUtils.reprojectGeoJson(testPoint, "EPSG:4326", "EPSG:900913");
+ expect(reprojectedTestPoint).toExist();
+ expect(reprojectedTestPoint.features).toExist();
+ expect(reprojectedTestPoint.features[0]).toExist();
+ expect(reprojectedTestPoint.features[0].type).toBe("Feature");
+ expect(reprojectedTestPoint.features[0].geometry.type).toBe("Point");
+ // approximate values should be the same
+ expect(reprojectedTestPoint.features[0].geometry.coordinates[0].toFixed(4)).toBe((-12523490.492568726).toFixed(4));
+ expect(reprojectedTestPoint.features[0].geometry.coordinates[1].toFixed(4)).toBe((5195238.005360028).toFixed(4));
+ });
});
diff --git a/web/client/utils/__tests__/PrintUtils-test.js b/web/client/utils/__tests__/PrintUtils-test.js
index 2cf2a9cde3..6b070e44db 100644
--- a/web/client/utils/__tests__/PrintUtils-test.js
+++ b/web/client/utils/__tests__/PrintUtils-test.js
@@ -15,7 +15,74 @@ const layer = {
params: {myparam: "myvalue"}
};
-
+const vectorLayer = {
+ "type": "vector",
+ "visibility": true,
+ "group": "Local shape",
+ "id": "web2014all_mv__14",
+ "name": "web2014all_mv",
+ "hideLoading": true,
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -112.50042920000001,
+ 42.22829164089942
+ ]
+ },
+ "properties": {
+ "serial_num": "12C324776"
+ },
+ "id": 0
+ }
+ ],
+ "style": {
+ "weight": 3,
+ "radius": 10,
+ "opacity": 1,
+ "fillOpacity": 0.1,
+ "color": "rgb(0, 0, 255)",
+ "fillColor": "rgb(0, 0, 255)"
+ }
+};
+const mapFishVectorLayer = {
+ "type": "Vector",
+ "name": "web2014all_mv",
+ "opacity": 1,
+ "styleProperty": "ms_style",
+ "styles": {
+ "1": {
+ "fillColor": "rgb(0, 0, 255)",
+ "fillOpacity": 0.1,
+ "pointRadius": 10,
+ "strokeColor": "rgb(0, 0, 255)",
+ "strokeOpacity": 1,
+ "strokeWidth": 3
+ }
+ },
+ "geoJson": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -12523490.492568726,
+ 5195238.005360028
+ ]
+ },
+ "properties": {
+ "serial_num": "12C324776",
+ "ms_style": 1
+ },
+ "id": 0
+ }
+ ]
+ }
+};
describe('PrintUtils', () => {
it('custom params are applied to wms layers', () => {
@@ -26,4 +93,31 @@ describe('PrintUtils', () => {
expect(specs[0].customParams.myparam).toExist();
expect(specs[0].customParams.myparam).toBe("myvalue");
});
+ it('vector layer generation for print', () => {
+ const specs = PrintUtils.getMapfishLayersSpecification([vectorLayer], {projection: "EPSG:3857"}, 'map');
+ expect(specs).toExist();
+ expect(specs.length).toBe(1);
+ expect(specs[0].geoJson.features[0].geometry.coordinates[0], mapFishVectorLayer).toBe(mapFishVectorLayer.geoJson.features[0].geometry.coordinates[0]);
+ });
+ it('vector layer default point style', () => {
+ const style = PrintUtils.getOlDefaultStyle({features: [{geometry: {type: "Point"}}]});
+ expect(style).toExist();
+ expect(style.pointRadius).toBe(5);
+ });
+ it('vector layer default marker style', () => {
+ const style = PrintUtils.getOlDefaultStyle({styleName: "marker", features: [{geometry: {type: "Point"}}]});
+ expect(style).toExist();
+ expect(style.externalGraphic).toExist();
+ });
+ it('vector layer default polygon style', () => {
+ const style = PrintUtils.getOlDefaultStyle({features: [{geometry: {type: "Polygon"}}]});
+ expect(style).toExist();
+ expect(style.strokeWidth).toBe(3);
+
+ });
+ it('vector layer default line style', () => {
+ const style = PrintUtils.getOlDefaultStyle({features: [{geometry: {type: "LineString"}}]});
+ expect(style).toExist();
+ expect(style.strokeWidth).toBe(3);
+ });
});
diff --git a/web/client/utils/openlayers/StyleUtils.js b/web/client/utils/openlayers/StyleUtils.js
index 5d18027f74..2ddd6d5cee 100644
--- a/web/client/utils/openlayers/StyleUtils.js
+++ b/web/client/utils/openlayers/StyleUtils.js
@@ -20,6 +20,14 @@ const toVectorStyle = function(layer, style) {
if (style.marker && (geomT === 'Point' || geomT === 'MultiPoint')) {
newLayer.styleName = "marker";
}else {
+ newLayer.style = {
+ weight: style.width,
+ radius: style.radius,
+ opacity: style.color.a,
+ fillOpacity: style.fill.a,
+ color: getColor(style.color),
+ fillColor: getColor(style.fill)
+ };
let stroke = new ol.style.Stroke({
color: getColor(style.color),
width: style.width