\n\n\n');
diff --git a/doc/user/index.rst b/doc/user/index.rst
index 49cb2597..d6e68c37 100644
--- a/doc/user/index.rst
+++ b/doc/user/index.rst
@@ -30,3 +30,10 @@ Supported databases
configuration
start-the-server
upgrade
+
+.. toctree::
+ :caption: Reports
+ :maxdepth: 2
+ :hidden:
+
+ report-types
diff --git a/doc/user/report-types.rst b/doc/user/report-types.rst
new file mode 100644
index 00000000..4a447e5c
--- /dev/null
+++ b/doc/user/report-types.rst
@@ -0,0 +1,6 @@
+Report types
+============
+
+.. toctree::
+
+ report-types/map
diff --git a/doc/user/report-types/image/map-empty.png b/doc/user/report-types/image/map-empty.png
new file mode 100644
index 00000000..d37437b8
Binary files /dev/null and b/doc/user/report-types/image/map-empty.png differ
diff --git a/doc/user/report-types/image/map-report-settings.png b/doc/user/report-types/image/map-report-settings.png
new file mode 100644
index 00000000..ceb1d4cf
Binary files /dev/null and b/doc/user/report-types/image/map-report-settings.png differ
diff --git a/doc/user/report-types/image/map-result.png b/doc/user/report-types/image/map-result.png
new file mode 100644
index 00000000..819cee4b
Binary files /dev/null and b/doc/user/report-types/image/map-result.png differ
diff --git a/doc/user/report-types/image/map-type-column-settings.png b/doc/user/report-types/image/map-type-column-settings.png
new file mode 100644
index 00000000..39915af2
Binary files /dev/null and b/doc/user/report-types/image/map-type-column-settings.png differ
diff --git a/doc/user/report-types/map.rst b/doc/user/report-types/map.rst
new file mode 100644
index 00000000..dd162742
--- /dev/null
+++ b/doc/user/report-types/map.rst
@@ -0,0 +1,76 @@
+Map
+===
+
+The *map* report type allows to display `GeoJSON `_ objects on a map.
+
+.. figure:: image/map-result.png
+
+ Example of a map report
+
+To use it, select the map marker icon.
+
+.. figure:: image/map-empty.png
+
+ Screenshot of an empty map report
+
+There are six different places where you can put your columns:
+
+GeoJSON column
+ This is the most important one. Place the column containing GeoJSON data here.
+
+ If your database does not contain GeoJSON data, you can probably convert
+ other formats to GeoJSON using SQL functions. For instance, MySQL has
+ `ST_AsGeoJSON `_
+ (you may need to modify the layer to use those functions).
+
+ Note that icons rendering works only for GeoJSON 'point' type and maybe for a 'multipoint' object (not tested).
+
+Label column
+ If you drop a column here, its text content will be shown next to the map marker.
+
+Value column
+ If you drop a column here, its numeric content will be used to calculate a
+ color intensity. Low values get low intensity and high values get high
+ intensity.
+
+Group column
+ If you drop a column here, its content will be used to group markers
+ together. If two rows have the same value in this column, they belong to the
+ same group. Each group is then assigned a unique color hue (automatically
+ chosen by Urungi) for the map marker.
+
+Type column
+ If you drop a column here, its content will be used to assign a type to a
+ row. Each type can then be associated with a specific icon to change the map
+ marker appearance.
+
+Filters
+ These are regular filters and work the same as for other report types.
+
+
+Report settings
+---------------
+
+.. figure:: image/map-report-settings.png
+
+ Screenshot of the map report settings modal window
+
+In the report settings you can change the map layer. It's the map that will be
+drawn under the GeoJSON objects. You can find different map layers on `the
+OpenStreetMap Wiki `_.
+
+
+Configure icons
+---------------
+
+If you use a *type* column, you can associate an icon with each type. To do
+that, click on the gear wheel icon next to the type column.
+
+.. figure:: image/map-type-column-settings.png
+
+ Screenshot of the map type column settings modal window
+
+Each distinct value contained in the type column will be displayed in the form.
+Associate an icon with a type by entering the icon's name. Only
+`Font Awesome 4 icons `_ are available at
+the moment.
diff --git a/package-lock.json b/package-lock.json
index fdbf88dd..e56093da 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
"js-xlsx": "^0.8.22",
"jsplumb": "^2.15.6",
"knex": "^0.21.21",
+ "leaflet": "^1.7.1",
"malihu-custom-scrollbar-plugin": "^3.1.5",
"migrate-mongo": "^8.2.3",
"moment": "^2.29.1",
@@ -12866,6 +12867,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/leaflet": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.7.1.tgz",
+ "integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
+ },
"node_modules/less": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz",
@@ -30022,6 +30028,11 @@
"flush-write-stream": "^1.0.2"
}
},
+ "leaflet": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.7.1.tgz",
+ "integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
+ },
"less": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz",
diff --git a/package.json b/package.json
index 51d5efc1..f3923c62 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"js-xlsx": "^0.8.22",
"jsplumb": "^2.15.6",
"knex": "^0.21.21",
+ "leaflet": "^1.7.1",
"malihu-custom-scrollbar-plugin": "^3.1.5",
"migrate-mongo": "^8.2.3",
"moment": "^2.29.1",
diff --git a/public/css/main.css b/public/css/main.css
index d305cff7..5241dd61 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -3181,3 +3181,13 @@ a.list-group-item:hover {
border-color: #999;
color: white;
}
+
+.leaflet-tooltip {
+ background: none;
+ border: none;
+ box-shadow: none;
+}
+
+.report-map-icon i {
+ font-size: 16px;
+}
diff --git a/public/images/gps-pin.png b/public/images/gps-pin.png
new file mode 100644
index 00000000..e6c95340
Binary files /dev/null and b/public/images/gps-pin.png differ
diff --git a/public/js/core/constants.js b/public/js/core/constants.js
index 552224c4..3f43f2c5 100644
--- a/public/js/core/constants.js
+++ b/public/js/core/constants.js
@@ -1,4 +1,4 @@
-/* global PNotify: false, PNotifyBootstrap3: false, PNotifyFontAwesome4: false, moment: false, numeral: false */
+/* global PNotify: false, PNotifyBootstrap3: false, PNotifyFontAwesome4: false, moment: false, numeral: false, L : false */
(function () {
'use strict';
@@ -11,5 +11,6 @@
.constant('PNotifyFontAwesome4', PNotifyFontAwesome4)
.constant('moment', moment)
.constant('numeral', numeral)
+ .constant('L', L)
.constant('base', base);
})();
diff --git a/public/js/report/controller.js b/public/js/report/controller.js
index 857e9821..e06a4730 100644
--- a/public/js/report/controller.js
+++ b/public/js/report/controller.js
@@ -110,6 +110,13 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
$scope.selectedReport.properties.pivotKeys = {};
$scope.selectedReport.properties.pivotKeys.columns = [];
$scope.selectedReport.properties.pivotKeys.rows = [];
+ $scope.selectedReport.properties.map = {
+ geojson: [],
+ value: [],
+ label: [],
+ group: [],
+ type: [],
+ };
$scope.selectedReport.properties.order = [];
$scope.selectedReport.properties.filters = [];
$scope.selectedReport.reportType = 'grid';
@@ -118,6 +125,7 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
$scope.selectedReport.properties.height = 300;
$scope.selectedReport.properties.range = '';
+ $scope.selectedReport.properties.mapLayerUrl = '';
$scope.selectedReport.properties.legendPosition = 'bottom';
@@ -149,6 +157,13 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
if (!report.properties.pivotKeys.rows) { report.properties.pivotKeys.rows = []; }
if (!report.properties.order) { report.properties.order = []; }
if (!report.properties.range) { report.properties.range = ''; }
+ if (!report.properties.map) { report.properties.map = {}; }
+ if (!report.properties.map.geojson) { report.properties.map.geojson = []; }
+ if (!report.properties.map.value) { report.properties.map.value = []; }
+ if (!report.properties.map.label) { report.properties.map.label = []; }
+ if (!report.properties.map.group) { report.properties.map.group = []; }
+ if (!report.properties.map.type) { report.properties.map.type = []; }
+ if (!report.properties.mapLayerUrl) { report.properties.mapLayerUrl = ''; }
};
/*
@@ -422,6 +437,12 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
role: 'column'
};
break;
+ case 'map':
+ choice = {
+ propertyBind: $scope.selectedReport.properties.ykeys,
+ role: 'column'
+ };
+ break;
}
return choice;
@@ -523,6 +544,14 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
report.reportType = 'pyramid';
break;
+ case 'map':
+ moveContent(report.properties.columns, movedColumns);
+ moveContent(report.properties.xkeys, movedColumns);
+ moveContent(report.properties.pivotKeys.columns, movedColumns);
+ moveContent(report.properties.pivotKeys.rows, movedColumns);
+ report.reportType = 'map';
+ break;
+
default:
notify.error(gettextCatalog.getString('report type does not exist'));
break;
@@ -639,6 +668,10 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
case 'pyramid':
available = report.properties.xkeys.length > 0 && report.properties.ykeys.length > 0;
break;
+
+ case 'map':
+ available = report.properties.map.geojson.length === 1;
+ break;
}
return available;
@@ -767,6 +800,7 @@ angular.module('app').controller('reportCtrl', function ($scope, connection, $co
$scope.selectedReport.properties.legendPosition = settings.legendPosition;
$scope.selectedReport.properties.height = settings.height;
$scope.selectedReport.properties.maxValue = settings.maxValue;
+ $scope.selectedReport.properties.mapLayerUrl = settings.mapLayerUrl;
$scope.selectedReport.theme = settings.theme;
$scope.selectedReport.properties.range = settings.range;
}, () => {});
diff --git a/public/js/report/directives/reportView.js b/public/js/report/directives/reportView.js
index 53fa9edd..3ff96cc6 100644
--- a/public/js/report/directives/reportView.js
+++ b/public/js/report/directives/reportView.js
@@ -1,4 +1,4 @@
-angular.module('app').directive('reportView', function ($q, $timeout, reportModel, $compile, c3Charts, reportHtmlWidgets, grid, pivot, uuid, gettextCatalog, api) {
+angular.module('app').directive('reportView', function ($q, $timeout, reportModel, $compile, c3Charts, reportHtmlWidgets, grid, pivot, map, uuid, gettextCatalog, api) {
return {
scope: {
@@ -84,6 +84,10 @@ angular.module('app').directive('reportView', function ($q, $timeout, reportMode
case 'indicator':
$scope.changeContent(reportHtmlWidgets.generateIndicator($scope.report, $scope.data));
break;
+
+ case 'map':
+ $scope.changeContent(``);
+ return map.createMap($scope.report, $scope.data);
}
} else {
$scope.changeContent('
' + gettextCatalog.getString('No data for this report') + '