diff options
Diffstat (limited to 'base_geoengine/static/src/js/views')
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); +}); |
