From 4359652d20996ebd10a3410ec354f94865704b70 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Thu, 1 Dec 2016 14:43:19 +0100 Subject: [PATCH] Fix #1314 Support to print vector layer (leaflet) - Support vector style for leaflet layers - Conversion to OL2 style format - add reprojectGeoJson utility method to CoordinateUtils Missing: - Print preview support - Markers --- web/client/utils/CoordinatesUtils.js | 92 +++++++++++++++- web/client/utils/PrintUtils.js | 103 ++++++++++++++++++ .../utils/__tests__/CoordinatesUtils-test.js | 29 +++++ web/client/utils/__tests__/PrintUtils-test.js | 92 +++++++++++++++- web/client/utils/openlayers/StyleUtils.js | 8 ++ 5 files changed, 319 insertions(+), 5 deletions(-) diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index c5ecbab537..99d1e2e231 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -5,11 +5,49 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -var Proj4js = require('proj4'); -var assign = require('object-assign'); -var {isArray, flattenDeep, chunk} = require('lodash'); +const Proj4js = require('proj4'); +const proj4 = Proj4js; +const assign = require('object-assign'); +const {isArray, flattenDeep, chunk, cloneDeep} = require('lodash'); +// Checks if `list` looks like a `[x, y]`. +function isXY(list) { + return list.length >= 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..da8419e05d 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,87 @@ 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", + // "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 { + "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..95c35d6036 100644 --- a/web/client/utils/__tests__/CoordinatesUtils-test.js +++ b/web/client/utils/__tests__/CoordinatesUtils-test.js @@ -72,4 +72,33 @@ 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"); + expect(reprojectedTestPoint.features[0].geometry.coordinates[0]).toBe(-12523490.492568726); + expect(reprojectedTestPoint.features[0].geometry.coordinates[1]).toBe(5195238.005360028); + }); }); diff --git a/web/client/utils/__tests__/PrintUtils-test.js b/web/client/utils/__tests__/PrintUtils-test.js index 2cf2a9cde3..46f1d9082d 100644 --- a/web/client/utils/__tests__/PrintUtils-test.js +++ b/web/client/utils/__tests__/PrintUtils-test.js @@ -7,6 +7,7 @@ */ const expect = require('expect'); const PrintUtils = require('../PrintUtils'); +const {isEqual} = require('lodash'); const layer = { url: "http://mygeoserver", @@ -15,7 +16,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 +94,26 @@ 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:900913"}, 'map'); + expect(specs).toExist(); + expect(specs.length).toBe(1); + expect(isEqual(specs[0], mapFishVectorLayer)).toBe(true); + }); + 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 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