summaryrefslogtreecommitdiff
path: root/base_geoengine/static/src/js/views
diff options
context:
space:
mode:
Diffstat (limited to 'base_geoengine/static/src/js/views')
-rw-r--r--base_geoengine/static/src/js/views/form_renderer.js20
-rw-r--r--base_geoengine/static/src/js/views/geoengine/geoengine_controller.js321
-rw-r--r--base_geoengine/static/src/js/views/geoengine/geoengine_renderer.js832
-rw-r--r--base_geoengine/static/src/js/views/geoengine/geoengine_view.js56
-rw-r--r--base_geoengine/static/src/js/views/view_registry.js14
5 files changed, 1243 insertions, 0 deletions
diff --git a/base_geoengine/static/src/js/views/form_renderer.js b/base_geoengine/static/src/js/views/form_renderer.js
new file mode 100644
index 0000000..024b4c2
--- /dev/null
+++ b/base_geoengine/static/src/js/views/form_renderer.js
@@ -0,0 +1,20 @@
+odoo.define('base_geoengine.form_renderer', function (require) {
+ "use strict";
+
+ var FormRenderer = require('web.FormRenderer');
+
+ FormRenderer.include({
+ // _updateView refreshes the view after a pager action.
+ // Add the TabListener to geo_type widget so that _renderMap is called
+ // when clicking on the new tab
+ _updateView: function ($newContent) {
+ var self=this;
+ this._super.apply(this, arguments);
+ _.each(this.allFieldWidgets[this.state.id], function (widget) {
+ if (widget.field.geo_type) {
+ widget._addTabListener()
+ }
+ });
+ }
+ });
+});
diff --git a/base_geoengine/static/src/js/views/geoengine/geoengine_controller.js b/base_geoengine/static/src/js/views/geoengine/geoengine_controller.js
new file mode 100644
index 0000000..62ac4e1
--- /dev/null
+++ b/base_geoengine/static/src/js/views/geoengine/geoengine_controller.js
@@ -0,0 +1,321 @@
+/* ---------------------------------------------------------
+ * Odoo base_geoengine
+ * Contributor Yannick Vaucher 2018 Camptocamp SA
+ * Copyright 2021 Shurshilov Artem
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+ */
+odoo.define('base_geoengine.GeoengineController', function (require) {
+ "use strict";
+
+ /**
+ * The Geoengine Controller controls the geo renderer and the geo model.
+ * Its role is to allow these two components to communicate properly, and
+ * also, to render and bind all extra actions in the 'Sidebar'.
+ */
+
+ var core = require('web.core');
+ var BasicController = require('web.BasicController');
+ var pyUtils = require('web.py_utils');
+ //var Sidebar = require('web.Sidebar');
+ var DataExport = require('web.DataExport');
+
+ var _t = core._t;
+
+ var GeoengineController = BasicController.extend({
+ custom_events: _.extend({}, BasicController.prototype.custom_events, {
+ selection_changed: '_onSelectionChanged',
+ }),
+
+ /**
+ * @class
+ * @override
+ * @param {Object} parent node
+ * @param {Boolean} params.editable
+ * @param {Boolean} params.hasSidebar
+ * @param {Object} params.toolbarActions
+ * @param {Boolean} params.noLeaf
+ */
+ init: function (parent, model, renderer, params) {
+ this._super.apply(this, arguments);
+ this.hasSidebar = params.hasSidebar;
+ this.toolbarActions = params.toolbarActions || {};
+ this.noLeaf = params.noLeaf;
+ this.selectedRecords = params.selectedRecords || [];
+
+ },
+
+ // --------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------
+
+ /**
+ * Calculate the active domain of the geo view. This should be done
+ * only if the header checkbox has been checked. This is done by
+ * evaluating the search results, and then adding the dataset domain
+ * (ie. action domain).
+ *
+ * @todo This is done only for the data export. The full mechanism is
+ * wrong, this method should be private, most of the code in the
+ * sidebar should be moved to the controller, and we should not use the
+ * getParent method...
+ *
+ * @returns {Deferred<Array[]>} a deferred that resolve to the active
+ * domain
+ */
+ getActiveDomain: function () {
+ // TODO: this method should be synchronous...
+ var self = this;
+ if (this.$('thead .o_list_record_selector input').prop('checked')) {
+ var searchView = this.getParent().searchview;
+ var searchData = searchView.build_search_data();
+ var userContext = this.getSession().user_context;
+ var results = pyUtils.eval_domains_and_contexts({
+ domains: searchData.domains,
+ contexts: [userContext].concat(searchData.contexts),
+ group_by_seq: searchData.groupbys || [],
+ });
+ var record = self.model.get(self.handle, {raw: true});
+ return $.when(record.getDomain().concat(results.domain || []));
+ }
+ return $.Deferred().resolve();
+ },
+
+ /**
+ * Returns the list of currently selected res_ids (with the check boxes
+ * on the left)
+ *
+ * @override
+ *
+ * @returns {Number[]} list of res_ids
+ */
+ getSelectedIds: function () {
+ return _.map(this.getSelectedRecords(), function (record) {
+ return record.res_id;
+ });
+ },
+
+ /**
+ * Returns the list of currently selected records (highlighted on the
+ * map)
+ *
+ * @returns {Object[]} list of records
+ */
+ getSelectedRecords: function () {
+ var self = this;
+ return _.map(this.selectedRecords, function (db_id) {
+ return self.model.get(db_id, {raw: true});
+ });
+ },
+
+ /**
+ * Render the sidebar (the 'action' menu in the control panel, right of
+ * the main buttons)
+ *
+ * @param {jQuery.Element} $node
+ */
+ renderSidebar: function ($node) {
+ if (this.hasSidebar && !this.sidebar) {
+ var other = [{
+ label: _t("Export"),
+ callback: this._onExportData.bind(this),
+ }];
+ if (this.archiveEnabled) {
+ other.push({
+ label: _t("Archive"),
+ callback: this._onToggleArchiveState.bind(this, true),
+ });
+ other.push({
+ label: _t("Unarchive"),
+ callback: this._onToggleArchiveState.bind(this, false),
+ });
+ }
+ if (this.is_action_enabled('delete')) {
+ other.push({
+ label: _t('Delete'),
+ callback: this._onDeleteSelectedRecords.bind(this),
+ });
+ }
+ this.sidebar = new Sidebar(this, {
+ editable: this.is_action_enabled('edit'),
+ env: {
+ context: this.model.get(this.handle, {
+ raw: true,
+ }).getContext(),
+ activeIds: this.getSelectedIds(),
+ model: this.modelName,
+ },
+ actions: _.extend(this.toolbarActions, {other: other}),
+ });
+ this.sidebar.appendTo($node);
+
+ this._toggleSidebar();
+ }
+ },
+
+ /**
+ * Overrides to update the list of selected records
+ *
+ * @override
+ */
+ update: function (params, options) {
+ var self = this;
+ if (options && options.keepSelection) {
+ // Filter out removed records from selection
+ var res_ids = this.model.get(this.handle).res_ids;
+ this.selectedRecords = _.filter(this.selectedRecords,
+ function (id) {
+ return _.contains(res_ids, self.model.get(id).res_id);
+ }
+ );
+ } else {
+ this.selectedRecords = [];
+ }
+ params.selectedRecords = this.selectedRecords;
+ return this._super.apply(this, arguments);
+ },
+
+ // --------------------------------------------------------------------
+ // Private
+ // --------------------------------------------------------------------
+
+ /**
+ * @see BasicController._abandonRecord
+ * If the given abandoned record is not the main one, notifies the
+ * renderer to remove the appropriate subrecord (line).
+ *
+ * @override
+ * @private
+ * @param {String} [recordID] - default to the main recordID
+ */
+ _abandonRecord: function (recordID) {
+ this._super.apply(this, arguments);
+ if ((recordID || this.handle) !== this.handle) {
+ var state = this.model.get(this.handle);
+ this.renderer.removeLine(state, recordID);
+ this._updatePager();
+ }
+ },
+
+ /**
+ * Adds a record to the list.
+ * Disables the buttons to prevent concurrent record creation or
+ * edition.
+ * TODO probably not needed
+ *
+ * @todo make record creation a basic controller feature
+ * @private
+ *
+ * @returns {Deferred} callback
+ */
+ _addRecord: function () {
+ var self = this;
+ this._disableButtons();
+ return this.renderer.unselectRow().then(function () {
+ return self.model.addDefaultRecord(self.handle, {
+ position: self.editable,
+ });
+ }).then(function (recordID) {
+ var state = self.model.get(self.handle);
+ self.renderer.updateState(state, {});
+ self.renderer.editRecord(recordID);
+ self._updatePager();
+ }).always(this._enableButtons.bind(this));
+ },
+
+ /**
+ * Archive the current selection
+ *
+ * @private
+ * @param {String[]} ids - ids of objects to (un)archive
+ * @param {Boolean} archive - to active or deactive
+ * @returns {Deferred} callback
+ */
+ _archive: function (ids, archive) {
+ if (ids.length === 0) {
+ return $.when();
+ }
+ return this.model
+ .toggleActive(ids, !archive, this.handle)
+ .then(this.update.bind(this, {}, {reload: false}));
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _getSidebarEnv: function () {
+ var env = this._super.apply(this, arguments);
+ var record = this.model.get(this.handle);
+ return _.extend(env, {domain: record.getDomain()});
+ },
+
+ /**
+ * Display the sidebar (the 'action' menu in the control panel)
+ * if we have some selected records.
+ */
+ _toggleSidebar: function () {
+ if (this.sidebar) {
+ this.sidebar.do_toggle(this.selectedRecords.length > 0);
+ }
+ },
+
+ /**
+ * @override
+ * @returns {Deferred}
+ */
+ _update: function () {
+ this._toggleSidebar();
+ return this._super.apply(this, arguments);
+ },
+
+ // --------------------------------------------------------------------
+ // Handlers
+ // --------------------------------------------------------------------
+
+ /**
+ * When the current selection changes (by clicking on the checkboxes on
+ * the left), we need to display (or hide) the 'sidebar'.
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onSelectionChanged: function (event) {
+ this.selectedRecords = event.data.selection;
+ this._toggleSidebar();
+ },
+
+ /**
+ * Called when clicking on 'Archive' or 'Unarchive' in the sidebar.
+ *
+ * @private
+ * @param {Boolean} archive
+ */
+ _onToggleArchiveState: function (archive) {
+ this._archive(this.selectedRecords, archive);
+ },
+
+ /**
+ * Opens the Export Dialog
+ *
+ * @private
+ */
+ _onExportData: function () {
+ var record = this.model.get(this.handle);
+ new DataExport(this, record).open();
+ },
+
+ /**
+ * Called when the 'delete' action is clicked on in the side bar.
+ *
+ * @private
+ */
+ _onDeleteSelectedRecords: function () {
+ this._deleteRecords(this.selectedRecords);
+ },
+
+ });
+
+ return GeoengineController;
+
+});
diff --git a/base_geoengine/static/src/js/views/geoengine/geoengine_renderer.js b/base_geoengine/static/src/js/views/geoengine/geoengine_renderer.js
new file mode 100644
index 0000000..edf0c4c
--- /dev/null
+++ b/base_geoengine/static/src/js/views/geoengine/geoengine_renderer.js
@@ -0,0 +1,832 @@
+/* ---------------------------------------------------------
+ * Odoo base_geoengine
+ * Contributor Yannick Vaucher 2018 Camptocamp SA
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+*/
+odoo.define('base_geoengine.GeoengineRenderer', function (require) {
+ "use strict";
+
+ var BasicRenderer = require('web.BasicRenderer');
+ var utils = require('web.utils');
+ var QWeb = require('web.QWeb');
+ var session = require('web.session');
+
+ var Record = require('base_geoengine.Record');
+ var GeoengineRecord = Record.GeoengineRecord;
+ var geoengine_common = require('base_geoengine.geoengine_common');
+ var BackgroundLayers = require('base_geoengine.BackgroundLayers');
+
+ /* CONSTANTS */
+ var DEFAULT_BEGIN_COLOR = "#FFFFFF";
+ var DEFAULT_END_COLOR = "#000000";
+ // For prop symbols only
+ var DEFAULT_MIN_SIZE = 5;
+ // For prop symbols only
+ var DEFAULT_MAX_SIZE = 15;
+ // For choroplets only
+ var DEFAULT_NUM_CLASSES = 5;
+ var LEGEND_MAX_ITEMS = 10;
+
+ /**
+ * Get display value of selection technical value
+ *
+ * @param {Object} field - odoo selection field
+ * @param {String} value - technical value for selection field
+ *
+ * @returns {String} Selection label for value
+ */
+ var _getSelectionLabel = function (field, value) {
+ for (var option in field.selection) {
+ if (field.selection[option][0] === value) {
+ return field.selection[option][1];
+ }
+ }
+ };
+
+ var _createFieldSpan = function (field, value) {
+ var label = field.string;
+ var val = value;
+ if (field.type === 'selection') {
+ val = _getSelectionLabel(field, val);
+ }
+ if (val instanceof Array) {
+ // TODO this needs a comment to know when a field in an Array
+ val = val[1];
+ }
+ if (val instanceof Object){
+ val = (val['data'] || {})['display_name'];
+ }
+ return '<span style="font-weight: bold">' +
+ label + '</span>: ' + val;
+ };
+
+ /**
+ * Formats attributes into a string for a single feature
+ *
+ * @param {Object} a - attributes
+ * @param {Object} fields - list of fields
+ *
+ * @returns {String} HTML content
+ */
+ var formatFeatureHTML = function (a, fields) {
+ var str = [];
+ var oid = '';
+ for (var key in a) {
+ if (Object.prototype.hasOwnProperty.call(a, key)) {
+ var val = a[key];
+ if (val === false) {
+ continue;
+ }
+ if (Object.prototype.hasOwnProperty.call(fields, key)) {
+ var field = fields[key];
+ var span = _createFieldSpan(field, val);
+ // ID field to put on first position
+ if (key === 'id') {
+ oid = span;
+ } else {
+ str.push(span);
+ }
+ }
+ }
+ }
+ str.unshift(oid);
+ return str.join('<br />');
+ };
+
+ /**
+ * Formats attributes into a string for multiple features
+ *
+ * @param {Array} features: features of the map
+ *
+ * @returns {String} HTML content
+ */
+ var formatFeatureListHTML = function (features) {
+ var str = [];
+ // Count unique record selected through all features
+ var selected_ids = [];
+ features.forEach(function (x) {
+ var rec_id = x.get('attributes').id;
+ if (selected_ids.indexOf(rec_id) < 0) {
+ selected_ids.push(rec_id);
+ }
+ });
+ str.push(selected_ids.length + ' selected records');
+ return str.join('<br />');
+ };
+
+ var GeoengineRenderer = BasicRenderer.extend(geoengine_common.GeoengineMixin, { // eslint-disable-line max-len
+
+ events: {
+ 'click #map_infobox': '_onInfoBoxClicked',
+ },
+
+ /**
+ * @class
+ * @param {Widget} parent
+ * @param {Any} state
+ * @param {Object} params
+ * @param {Boolean} params.hasSelectors
+ */
+ init: function (parent, state, params) {
+ this._super.apply(this, arguments);
+
+ this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
+
+ this.viewInfo = params.viewInfo;
+ this.mapOptions = {
+ 'geoengine_layers': params.viewInfo.geoengine_layers,
+ };
+ this.bgLayers = new BackgroundLayers();
+
+ this.selection = [];
+ this.overlaysGroup = null;
+ this.vectorSources = [];
+ this.zoomToExtentCtrl = null;
+ this.popupElement = null;
+ this.overlayPopup = null;
+ this.featurePopup = null;
+ this.ids = {};
+
+ this.geometryFields = [];
+ _.each(this.mapOptions.geoengine_layers.actives, function (item) {
+ this.geometryFields.push(item.geo_field_id[1]);
+ }.bind(this));
+ },
+
+ /**
+ * @override
+ * Called each time the renderer is attached into the DOM.
+ *
+ * Redraw the map and its vector layers
+ */
+ on_attach_callback: function () {
+ var map = this._renderMap();
+ $('#olmap').data('map', map);
+ this._renderVectorLayers();
+ return this._super();
+ },
+
+ // --------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ updateState: function () {
+ var def = this._super.apply(this, arguments);
+ this._renderVectorLayers();
+ return def;
+ },
+
+ willStart: function () {
+ var arch = this.viewInfo.arch;
+ _.each(arch.children, function (child) {
+ if (child.tag === 'templates') {
+ this.qweb.add_template(utils.json_node_to_xml(child));
+ }
+ }.bind(this));
+ return this._super();
+ },
+
+ // --------------------------------------------------------------------
+ // Private
+ // --------------------------------------------------------------------
+
+ /**
+ * Render the map
+ *
+ * @private
+ * @returns {jQueryElement} a jquery element <tbody>
+ */
+ _renderMap: function () {
+ if (_.isUndefined(this.map)) {
+ this.zoomToExtentCtrl = new ol.control.ZoomToExtent();
+ var backgrounds = this.mapOptions.geoengine_layers.backgrounds;
+ this.map = new ol.Map({
+ layers: [new ol.layer.Group({
+ title: 'Base maps',
+ layers: this.bgLayers.create(backgrounds),
+ })],
+ target: 'olmap',
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+ controls: ol.control.defaults().extend([
+ new ol.control.FullScreen(),
+ new ol.control.ScaleLine(),
+ new ol.control.LayerSwitcher(),
+ this.zoomToExtentCtrl,
+ ]),
+ });
+ this._registerInteraction();
+ }
+ return this.map;
+ },
+
+ /**
+ * Main render function for the map. It is rendered as a div.
+ *
+ * @override
+ * @private
+ * returns {Deferred} this deferred is resolved immediately
+ */
+ _renderView: function () {
+ var $map_div = $('<div>', {id: 'olmap', class: 'olmap'});
+ var $map_infobox = $('<div>', {
+ id: 'map_infobox',
+ class: 'map_overlay',
+ });
+ var $map_info_open = $('<div>', {id: 'map_info_open'});
+
+ $map_info_open.append($('<span>', {class: 'fa fa-edit'}));
+ $map_infobox.append($map_info_open);
+
+ var $map_info_filter_selection = $('<div>', {
+ id: 'map_info_filter_selection',
+ });
+ var $filter_selection = $('<span>', {class: 'fa fa-filter'});
+ $filter_selection.append('Filter selection');
+ $map_info_filter_selection.append($filter_selection);
+ $map_infobox.append($map_info_filter_selection);
+
+ $map_infobox.append($('<div>', {id: 'map_info'}));
+
+ $map_div.append($map_infobox);
+
+ this.$el.addClass('o_geo_view').append($map_div);
+
+ return this._super();
+ },
+
+ _hidePopup: function () {
+ this.overlayPopup.setPosition(undefined);
+ },
+
+ /**
+ * Method: _createVectorLayer
+ *
+ * Parameters:
+ * @param {Object} cfg - config object specific to the vector layer
+ * @param {Array} data - array of features
+ *
+ * @returns {ol.layer.Vector}: One Vector layer containing the features
+ */
+ _createVectorLayer: function (cfg, data) {
+ if (!data.length) {
+ return new ol.layer.Vector({
+ source: new ol.source.Vector(),
+ title: cfg.name,
+ });
+ }
+ var vectorSource = new ol.source.Vector();
+ _.each(data, function (item) {
+ var attributes = _.clone(item.data);
+ _.each(this.geometryFields, function (geo_field) {
+ delete attributes[geo_field];
+ });
+
+ if (cfg.display_polygon_labels === true) {
+ attributes.label = item[cfg.attribute_field_id[1]];
+ } else {
+ attributes.label = '';
+ }
+
+ var json_geometry = item.data[cfg.geo_field_id[1]];
+ if (json_geometry) {
+ var feature = new ol.Feature({
+ geometry: new ol.format.GeoJSON()
+ .readGeometry(json_geometry),
+ attributes: attributes,
+ });
+ var id = String(attributes.id);
+ this.ids[id] = item.id;
+ vectorSource.addFeature(feature);
+ }
+ }.bind(this));
+ var styleInfo = this._styleVectorLayer(cfg, data);
+ // Init legend
+ var parentContainer = this.$el.find('#map_legend');
+ var elLegend = $(styleInfo.legend || '<div/>');
+ elLegend.hide();
+ parentContainer.append(elLegend);
+ var lv = new ol.layer.Vector({
+ source: vectorSource,
+ title: cfg.name,
+ active_on_startup: cfg.active_on_startup,
+ style: styleInfo.style,
+ });
+ lv.on('change:visible', function () {
+ if (lv.getVisible()) {
+ elLegend.show();
+ } else {
+ elLegend.hide();
+ }
+ });
+ this.vectorSources.push(vectorSource);
+ if (cfg.layer_opacity) {
+ lv.setOpacity(cfg.layer_opacity);
+ }
+ return lv;
+ },
+
+ _extractLayerValues: function (cfg, data) {
+ var values = [];
+ var indicator = cfg.attribute_field_id[1];
+ _.each(data, function (item) {
+ values.push(item.data[indicator]);
+ });
+ return values;
+ },
+
+ _styleVectorLayerColored: function (cfg, data) {
+ var indicator = cfg.attribute_field_id[1];
+ var values = this._extractLayerValues(cfg, data);
+ var nb_class = cfg.nb_class || DEFAULT_NUM_CLASSES;
+ var opacity = 0.8;
+ var begin_color_hex = cfg.begin_color || DEFAULT_BEGIN_COLOR;
+ var end_color_hex = cfg.end_color || DEFAULT_END_COLOR;
+ var begin_color = chroma(begin_color_hex).alpha(opacity).css();
+ var end_color = chroma(end_color_hex).alpha(opacity).css();
+ var scale = chroma.scale([begin_color, end_color]);
+ var serie = new geostats(values);
+ var vals = null;
+
+ switch (cfg.classification) {
+ case 'unique':
+ case 'custom':
+ vals = serie.getClassUniqueValues();
+ scale = chroma.scale('RdYlBu').domain(
+ [0, vals.length], vals.length);
+ break;
+ case 'quantile':
+ serie.getClassQuantile(nb_class);
+ vals = serie.getRanges();
+ scale = scale.domain([0, vals.length], vals.length);
+ break;
+ case 'interval':
+ serie.getClassEqInterval(nb_class);
+ vals = serie.getRanges();
+ scale = scale.domain([0, vals.length], vals.length);
+ break;
+ }
+ var colors = [];
+ if (cfg.classification === 'custom') {
+ _.each(vals, function (val) {
+ if (val) {
+ colors.push(chroma(val).alpha(opacity).css());
+ }
+ });
+ } else {
+ _.each(scale.colors(), function (color) {
+ colors.push(chroma(color).alpha(opacity).css());
+ });
+ }
+ var styles_map = {};
+ _.each(colors, function (color) {
+ if (color in styles_map) {
+ return;
+ }
+ var fill = new ol.style.Fill({
+ color: color,
+ });
+ var stroke = new ol.style.Stroke({
+ color: '#333333',
+ width: 2,
+ });
+ var styles = [
+ new ol.style.Style({
+ image: new ol.style.Circle({
+ fill: fill,
+ stroke: stroke,
+ radius: 7,
+ }),
+ fill: fill,
+ stroke: stroke,
+ }),
+ ];
+ styles_map[color] = styles;
+ });
+ var legend = null;
+ if (vals.length <= LEGEND_MAX_ITEMS) {
+ serie.setColors(colors);
+ legend = serie.getHtmlLegend(null, cfg.name, 1);
+ }
+ return {
+ style: function (feature) {
+ var value = feature.get('attributes')[indicator];
+ var color_idx = this._getClass(value, vals);
+ return styles_map[colors[color_idx]];
+ }.bind(this),
+ legend: legend,
+ };
+ },
+
+ _styleVectorLayerProportion: function (cfg, data) {
+ var indicator = cfg.attribute_field_id[1];
+ var values = this._extractLayerValues(cfg, data);
+ var serie = new geostats(values);
+ var styles_map = {};
+ var minSize = cfg.min_size || DEFAULT_MIN_SIZE;
+ var maxSize = cfg.max_size || DEFAULT_MAX_SIZE;
+ var minVal = serie.min();
+ var maxVal = serie.max();
+ // TODO to be defined on cfg
+ var opacity = 0.8;
+ var color_hex = cfg.begin_color || DEFAULT_BEGIN_COLOR;
+ var color = chroma(color_hex).alpha(opacity).css();
+ var fill = new ol.style.Fill({
+ color: color,
+ });
+ var stroke = new ol.style.Stroke({
+ color: '#333333',
+ width: 2,
+ });
+ _.each(values, function (value) {
+ if (value in styles_map) {
+ return;
+ }
+ var proportion = (value - minVal) / (maxVal - minVal);
+ var proportion_sized = proportion * (maxSize - minSize);
+ var radius = proportion_sized + minSize;
+ var styles = [
+ new ol.style.Style({
+ image: new ol.style.Circle({
+ fill: fill,
+ stroke: stroke,
+ radius: radius,
+ }),
+ fill: fill,
+ stroke: stroke,
+ }),
+ ];
+ styles_map[value] = styles;
+ });
+
+ return {
+ style: function (feature) {
+ var value = feature.get('attributes')[indicator];
+ return styles_map[value];
+ },
+ legend : '',
+ };
+ },
+
+ _styleVectorLayerDefault: function (cfg, data) {
+ // TODO to be defined on cfg
+ var opacity = 0.8;
+ var color_hex = cfg.begin_color || DEFAULT_BEGIN_COLOR;
+ var color = chroma(color_hex).alpha(opacity).css();
+ // Basic
+ var fill = new ol.style.Fill({
+ color: color,
+ });
+ var stroke = new ol.style.Stroke({
+ color: '#333333',
+ width: 2,
+ });
+ var olStyleText = new ol.style.Text({
+ text: '',
+ fill: new ol.style.Fill({
+ color: '#000000',
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#FFFFFF',
+ width: 5,
+ }),
+ });
+ var styles = [
+ new ol.style.Style({
+ image: new ol.style.Circle({
+ fill: fill,
+ stroke: stroke,
+ radius: this._getBasicCircleRadius(cfg, data),
+ }),
+ fill: fill,
+ stroke: stroke,
+ text: olStyleText,
+ }),
+ ];
+ return {
+ style: function (feature) {
+ var label_text = feature.values_.attributes.label;
+ if (label_text === false) {
+ label_text = '';
+ }
+ styles[0].text_.text_ = label_text;
+ return styles;
+ },
+ legend: '',
+ };
+ },
+
+ _styleVectorLayer: function (cfg, data) {
+
+ switch (cfg.geo_repr) {
+ case 'colored':
+ return this._styleVectorLayerColored(cfg, data);
+ case 'proportion':
+ return this._styleVectorLayerProportion(cfg, data);
+ default:
+ return this._styleVectorLayerDefault(cfg, data);
+ }
+ },
+
+ _getBasicCircleRadius: function () {
+ return 5;
+ },
+
+ _getClass: function (val, a) {
+ // Classification uniqueValues
+ var idx = a.indexOf(val);
+ if (idx > -1) {
+ return idx;
+ }
+ // Range classification
+ var separator = ' - ';
+ for (var i = 0; i < a.length; i++) {
+ // All classification except uniqueValues
+ if (a[i].indexOf(separator) !== -1) {
+ var item = a[i].split(separator);
+ if (val <= parseFloat(item[1])) {
+ return i;
+ }
+ } else if (val === a[i]) {
+ // Classification uniqueValues
+ return i;
+ }
+ }
+ },
+
+ /**
+ * Creates vector layers from config and populates them with features
+ *
+ * Parameters:
+ * @param {Array} data - vector data to be added to the vector layer
+ *
+ * @returns {Array} - array of vector layers
+ */
+ _createVectorLayers: function (data) {
+ var out = [];
+ var layers = this.mapOptions.geoengine_layers.actives;
+ _.each(layers, function (item) {
+ out.push(this._createVectorLayer(item, data));
+ }.bind(this));
+ return out;
+ },
+
+ _createPopupOverlay: function () {
+ if (this.overlayPopup !== null) {
+ return this.overlayPopup;
+ }
+
+ var $popup = this.$('.layer-popup');
+ var popupElement = $popup.get(0);
+ var overlayPopup = new ol.Overlay({
+ element: popupElement,
+ positioning: 'bottom-center',
+ stopEvent: false,
+ });
+ this.popupElement = popupElement;
+ this.overlayPopup = overlayPopup;
+ return overlayPopup;
+ },
+
+ _getGeoengineRecord: function (record) {
+ var record_options = {
+ fields: this.viewInfo.fields,
+ model: this.model,
+ qweb: this.qweb,
+ };
+ return new GeoengineRecord(this, record, record_options);
+ },
+
+ _showFeaturePopup: function (feature) {
+ var template_name = 'layer-box';
+ if (this.qweb.templates[template_name]) {
+ var coord = ol.extent.getCenter(
+ feature.getGeometry().getExtent());
+ this.overlayPopup.setPosition(coord);
+ this.featurePopup = feature;
+ var record = feature.get('attributes');
+ var geoengine_record = this._getGeoengineRecord(record);
+ this.$('.popup-content').empty();
+ geoengine_record.appendTo(this.$('.popup-content'));
+ }
+ },
+
+ _renderVectorLayers: function () {
+ var data = this.state.data;
+
+ this.map.removeLayer(this.overlaysGroup);
+ this.ids = {};
+
+ var vectorLayers = this._createVectorLayers(data);
+ this.overlaysGroup = new ol.layer.Group({
+ title: 'Overlays',
+ layers: vectorLayers,
+ });
+
+ this._createPopupOverlay();
+
+ _.each(vectorLayers, function (vlayer) {
+ // First vector always visible on startup
+ if (vlayer !== vectorLayers[0] &&
+ !vlayer.values_.active_on_startup) {
+ vlayer.setVisible(false);
+ }
+ });
+ this.map.addLayer(this.overlaysGroup);
+ this.map.addOverlay(this.overlayPopup);
+
+ // Zoom to data extent
+ if (data.length) {
+ var extent = vectorLayers[0].getSource().getExtent();
+ this.zoomToExtentCtrl.extent_ = extent;
+ this.zoomToExtentCtrl.changed();
+
+ // When user quits fullscreen map, the size is set to undefined
+ // So we have to check this and recompute the size.
+ if (typeof this.map.getSize() === 'undefined' ) {
+ this.map.updateSize();
+ }
+ if (!ol.extent.isEmpty(extent)) {
+ this.map.getView().fit(extent);
+ }
+ }
+ },
+
+ _updateInfoBoxSingle: function (features) {
+ var $map_info = $("#map_info");
+ var $map_infobox = $("#map_infobox");
+ var $map_info_open = $('#map_info_open');
+ var $map_info_filter_selection = $("#map_info_filter_selection");
+
+ var feature = features.item(0);
+ var attributes = feature.get('attributes');
+ $map_info.html(formatFeatureHTML(attributes, this.viewInfo.fields));
+ var id = attributes.id;
+ if (id in this.ids) {
+ $map_infobox.data('id', this.ids[id]);
+ }
+ $map_info_open.show();
+ $map_info_filter_selection.hide();
+ $map_infobox.show();
+ },
+
+ _updateInfoBoxMulti: function (features) {
+ var $map_info = $("#map_info");
+ var $map_infobox = $("#map_infobox");
+ var $map_info_open = $('#map_info_open');
+ var $map_info_filter_selection = $("#map_info_filter_selection");
+
+ // Several selected features
+ $map_info.html(formatFeatureListHTML(features));
+ var selected_ids = [];
+ features.forEach(function (feature) {
+ var attributes = feature.get('attributes');
+ selected_ids.push(attributes.id);
+ });
+ $map_infobox.data('ids', selected_ids);
+ $map_info_open.hide();
+ $map_info_filter_selection.show();
+ $map_infobox.show();
+ },
+
+ _updateInfoBox: function (features) {
+
+ var nbFeatures = features.getLength();
+
+ var $map_infobox = $("#map_infobox");
+ $map_infobox.data('id', null);
+ $map_infobox.data('ids', []);
+
+ if (!nbFeatures) {
+ $map_infobox.hide();
+ this._hidePopup();
+ return;
+ }
+
+ if (nbFeatures === 1) {
+ this._updateInfoBoxSingle(features);
+ } else {
+ this._updateInfoBoxMulti(features);
+ }
+ },
+
+ _getCustomSelectedStyleFunction: function () {
+ var self = this;
+ return function (feature) {
+ var geometryType = feature.getGeometry().getType();
+ var styles = ol.style.Style.createDefaultEditing();
+ var geometryStyle = styles[geometryType];
+ if (geometryType !== ol.geom.GeometryType.POINT) {
+ return geometryStyle;
+ }
+
+ var geometryStylePoint = geometryStyle[0];
+ return [new ol.style.Style({
+ fill: geometryStylePoint.getFill(),
+ stroke: geometryStylePoint.getStroke(),
+ image: new ol.style.Circle({
+ fill: geometryStylePoint.image_.getFill(),
+ stroke: geometryStylePoint.image_.getStroke(),
+ radius: self._getBasicCircleRadius(),
+ }),
+ })];
+ };
+ },
+
+ _registerInteraction: function () {
+ // Select interaction working on "click"
+
+ var selectClick = new ol.interaction.Select({
+ condition: ol.events.condition.click,
+ style: this._getCustomSelectedStyleFunction(),
+ });
+ selectClick.on('select', function (e) {
+ var features = e.target.getFeatures();
+ this._updateInfoBox(features);
+
+ if (features.getLength() > 0) {
+ var feature = features.item(0);
+ this._showFeaturePopup(feature);
+ }
+ }.bind(this));
+
+ // Select interaction working on "pointermove"
+ var selectPointerMove = new ol.interaction.Select({
+ condition: ol.events.condition.pointerMove,
+ style: this._getCustomSelectedStyleFunction(),
+ });
+
+ // A DragBox interaction used to select features by drawing boxes
+ var dragBox = new ol.interaction.DragBox({
+ condition: ol.events.condition.shiftKeyOnly,
+ style: new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: [0, 0, 255, 1],
+ }),
+ }),
+ });
+
+ var selectedFeatures = selectClick.getFeatures();
+ dragBox.on('boxend', function () {
+ // Features that intersect the box are added to the collection
+ // of selected features, and their names are displayed in the
+ // "info" div
+ var extent = dragBox.getGeometry().getExtent();
+ var layerVectors = this.map.getLayers().item(1).getLayers();
+ var vectorSource = null;
+ layerVectors.forEach(function (lv) {
+ // Enable selection only on visible layers
+ if (lv.getVisible()) {
+ vectorSource = lv.getSource();
+ vectorSource.forEachFeatureIntersectingExtent(
+ extent, function (feature) {
+ if (selectedFeatures.getArray()
+ .indexOf(feature) < 0) {
+ selectedFeatures.push(feature);
+ }
+ }
+ );
+ }
+ });
+ this._updateInfoBox(selectedFeatures);
+ }.bind(this));
+
+ this.map.addInteraction(dragBox);
+ this.map.addInteraction(selectClick);
+ this.map.addInteraction(selectPointerMove);
+ },
+
+ // --------------------------------------------------------------------
+ // Handlers
+ // --------------------------------------------------------------------
+
+ _onInfoBoxClicked: function (event) {
+ if (!$(event.target).prop('special_click')) {
+ var id = $(event.currentTarget).data('id');
+ var ids = $(event.currentTarget).data('ids');
+ if (id) {
+ this.trigger_up(
+ 'open_record', {id: id, target: event.target}
+ );
+ } else if (ids.length) {
+ // TODO restore "Geo selection" item in search view field
+ // using icon fa-map-o currently the selection cannot be
+ // cancelled as it was in Odoo 10.0
+ this.trigger_up('search', {
+ domains: [[['id', 'in', ids]]],
+ contexts: [],
+ groupbys: [],
+ });
+ }
+ }
+ },
+ });
+
+ return GeoengineRenderer;
+});
diff --git a/base_geoengine/static/src/js/views/geoengine/geoengine_view.js b/base_geoengine/static/src/js/views/geoengine/geoengine_view.js
new file mode 100644
index 0000000..1994eb1
--- /dev/null
+++ b/base_geoengine/static/src/js/views/geoengine/geoengine_view.js
@@ -0,0 +1,56 @@
+/* ---------------------------------------------------------
+ * Odoo base_geoengine
+ * Author B.Binet Copyright Camptocamp SA
+ * Contributor N. Bessi Copyright Camptocamp SA
+ * Contributor Laurent Mignon 2015 Acsone SA/NV
+ * Contributor Yannick Vaucher 2015-2018 Camptocamp SA
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+*/
+odoo.define('base_geoengine.GeoengineView', function (require) {
+ "use strict";
+
+ /* ---------------------------------------------------------
+ * Odoo geoengine view
+ * ---------------------------------------------------------
+ */
+
+ var core = require('web.core');
+ var BasicView = require('web.BasicView');
+ var GeoengineController = require('base_geoengine.GeoengineController');
+ var GeoengineRenderer = require('base_geoengine.GeoengineRenderer');
+ var geoengine_common = require('base_geoengine.geoengine_common');
+
+ var _lt = core._lt;
+
+ var GeoengineView = BasicView.extend(geoengine_common.GeoengineMixin, {
+ accesskey: 'g',
+ display_name: _lt('Geoengine'),
+ icon: 'fa-map-o',
+ config: _.extend({}, BasicView.prototype.config, {
+ Renderer: GeoengineRenderer,
+ Controller: GeoengineController,
+ }),
+ viewType: 'geoengine',
+ template: 'GeoengineView',
+
+ /**
+ * @override
+ *
+ * @param {Object} viewInfo
+ * @param {Object} params
+ * @param {Boolean} params.sidebar
+ * @param {Boolean} [params.hasSelectors=true]
+ */
+ init: function (viewInfo, params) {
+ this._super.apply(this, arguments);
+
+ this.controllerParams.hasSidebar = params.sidebar;
+ this.rendererParams.viewInfo = viewInfo;
+
+ this.loadParams.limit = 1000;
+ },
+ });
+
+ return GeoengineView;
+});
diff --git a/base_geoengine/static/src/js/views/view_registry.js b/base_geoengine/static/src/js/views/view_registry.js
new file mode 100644
index 0000000..418ae6d
--- /dev/null
+++ b/base_geoengine/static/src/js/views/view_registry.js
@@ -0,0 +1,14 @@
+/* ---------------------------------------------------------
+ * Odoo base_geoengine
+ * Author Yannick Vaucher Copyright 2018 Camptocamp SA
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+ */
+odoo.define('base_geoengine._view_registry', function (require) {
+ "use strict";
+
+ var GeoengineView = require('base_geoengine.GeoengineView');
+ var view_registry = require('web.view_registry');
+
+ view_registry.add('geoengine', GeoengineView);
+});