summaryrefslogtreecommitdiff
path: root/base_geoengine/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'base_geoengine/static/src')
-rw-r--r--base_geoengine/static/src/css/style.css180
-rw-r--r--base_geoengine/static/src/images/editing_tool_bar.pngbin0 -> 2222 bytes
-rw-r--r--base_geoengine/static/src/js/.eslintrc9
-rw-r--r--base_geoengine/static/src/js/geoengine_common.js148
-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
-rw-r--r--base_geoengine/static/src/js/widgets/geoengine_record.js74
-rw-r--r--base_geoengine/static/src/js/widgets/geoengine_template_widgets.js104
-rw-r--r--base_geoengine/static/src/js/widgets/geoengine_widgets.js586
-rw-r--r--base_geoengine/static/src/xml/geoengine.xml116
13 files changed, 2460 insertions, 0 deletions
diff --git a/base_geoengine/static/src/css/style.css b/base_geoengine/static/src/css/style.css
new file mode 100644
index 0000000..3d1ca68
--- /dev/null
+++ b/base_geoengine/static/src/css/style.css
@@ -0,0 +1,180 @@
+/* OpenLayers Style */
+div.olmap {
+ /* For map size recomputation we have to define this size. */
+ height: 100%;
+ width: 100%;
+ z-index: 0;
+ padding: 0 !important;
+ margin: 0 !important;
+ cursor: default;
+}
+
+.ol-control{
+ background-image: none;
+}
+.ol-control button{
+ background-image: none;
+}
+
+.olmap svg{
+ width: initial;
+ height: initial;
+
+}
+
+.olmap:-moz-full-screen {
+ height: 100%;
+ width: 100%;
+}
+.olmap:-webkit-full-screen {
+ height: 100%;
+ width: 100%;
+}
+.olmap:-ms-fullscreen {
+ height: 100%;
+ width: 100%;
+}
+
+div.olControlPanel {
+ margin: 80px 10px;
+}
+
+div.ol-clear {
+ top: 65px;
+ left: .5em;
+}
+.olmap:fullscreen {
+ height: 100%;
+ width: 100%;
+}
+
+.olControlEditingToolbar .olControlSelectBoxItemInactive {
+ background-position: 0px -1px;
+}
+
+.olControlEditingToolbar .olControlSelectBoxItemActive {
+ background-position: 0px -24px;
+}
+
+.olDrowBox {
+ cursor: crosshair;
+}
+.map_overlay {
+ position: absolute;
+ display: none;
+ background: white;
+ opacity: 0.9;
+ border: 2px solid black;
+ z-index: 3000;
+ padding: 0 0 5px 0;
+}
+#map_infobox {
+ bottom: 10px;
+ left: 10px;
+ width: 200px;
+ height: 150px;
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+}
+#map_info {
+ cursor: pointer;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 10px;
+ height: 130px;
+}
+#map_info_open {
+ cursor: pointer;
+ background-color: #337ab7;
+ vertical-align: middle;
+ text-align: center;
+ color: white;
+}
+#map_info_filter_selection {
+ cursor: pointer;
+ background-color: #337ab7;
+ vertical-align: middle;
+ text-align: center;
+ color: white;
+}
+
+#map_measurementbox {
+ top: 18px;
+ left: 160px;
+ padding: 2px 5px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+}
+
+#map_legend {
+ bottom: 6em;
+ right: 5em;
+ z-index: 3000;
+}
+
+/* Button style */
+.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_geoengine:after {
+ content: "2";
+}
+
+/* Edit Widget */
+.field_geo_edit_map {
+ border: 1px solid #999;
+ width: 100%;
+ height: 500px;
+}
+
+.ol-popup {
+ position: absolute;
+ background-color: white;
+ -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+ filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+ padding: 15px;
+ border-radius: 10px;
+ border: 1px solid #cccccc;
+ bottom: 12px;
+ left: -50px;
+ min-width: 280px;
+}
+
+.ol-popup:after, .ol-popup:before {
+ top: 100%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.ol-popup:after {
+ border-top-color: white;
+ border-width: 10px;
+ left: 48px;
+ margin-left: -10px;
+}
+
+.ol-popup:before {
+ border-top-color: #cccccc;
+ border-width: 11px;
+ left: 48px;
+ margin-left: -11px;
+}
+
+.ol-popup-edit {
+ text-decoration: none;
+ position: absolute;
+ top: 5px;
+ right: 25px;
+}
+
+.ol-popup-closer {
+ text-decoration: none;
+ position: absolute;
+ top: 2px;
+ right: 8px;
+}
+
+.ol-popup-closer:after {
+ content: "✖";
+}
diff --git a/base_geoengine/static/src/images/editing_tool_bar.png b/base_geoengine/static/src/images/editing_tool_bar.png
new file mode 100644
index 0000000..5977856
--- /dev/null
+++ b/base_geoengine/static/src/images/editing_tool_bar.png
Binary files differ
diff --git a/base_geoengine/static/src/js/.eslintrc b/base_geoengine/static/src/js/.eslintrc
new file mode 100644
index 0000000..92803c8
--- /dev/null
+++ b/base_geoengine/static/src/js/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "globals": {
+ "$": false,
+ "_": false,
+ "ol": false,
+ "chroma": false,
+ "geostats": false,
+ }
+}
diff --git a/base_geoengine/static/src/js/geoengine_common.js b/base_geoengine/static/src/js/geoengine_common.js
new file mode 100644
index 0000000..ffc565d
--- /dev/null
+++ b/base_geoengine/static/src/js/geoengine_common.js
@@ -0,0 +1,148 @@
+/* ---------------------------------------------------------
+ * 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-2017 Camptocamp SA
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+ */
+odoo.define('base_geoengine.geoengine_common', function () {
+ "use strict";
+
+ /* ---------------------------------------------------------
+ * Odoo geoengine view
+ * ---------------------------------------------------------
+ */
+
+ var GeoengineMixin = {
+ cssLibs: [
+ '/base_geoengine/static/lib/ol-4.6.5/ol.css',
+ '/base_geoengine/static/lib/ol3-layerswitcher.css',
+ '/base_geoengine/static/lib/geostats-1.4.0/geostats.css',
+ ],
+ jsLibs: [
+ '/base_geoengine/static/lib/ol-4.6.5/ol-debug.js',
+ '/base_geoengine/static/lib/ol3-layerswitcher.js',
+ '/base_geoengine/static/lib/chromajs-0.8.0/chroma.js',
+ '/base_geoengine/static/lib/geostats-1.4.0/geostats.js',
+ ],
+ };
+
+ return {
+ GeoengineMixin: GeoengineMixin,
+ };
+});
+
+
+odoo.define('base_geoengine.BackgroundLayers', function (require) {
+ "use strict";
+
+ var Class = require('web.Class');
+
+ var BackgroundLayers = Class.extend({
+
+ /**
+ * Creates background layers from config
+ *
+ * @param {Array} layersCfg - the background layers array of config
+ * objects
+ * @returns {Array} - backgound layers
+ */
+ create: function (layersCfg) {
+ var out = [];
+ _.each(layersCfg, function (l) {
+ if (l.is_wmts) {
+ var source_opt = {
+ layer: l.name,
+ matrixSet: l.matrix_set,
+ style: 'default',
+ };
+ var tilegrid_opt = {};
+ var layer_opt = {
+ title: l.name,
+ visible: !l.overlay,
+ type: 'base',
+ };
+
+ var urls = l.url.split(',');
+ if (urls.length > 1) {
+ source_opt.urls = urls;
+ } else {
+ source_opt.url = urls[0];
+ }
+
+ if (l.format_suffix) {
+ source_opt.format = l.format_suffix;
+ }
+ if (l.request_encoding) {
+ source_opt.requestEncoding = l.request_encoding;
+ }
+ if (l.projection) {
+ source_opt.projection = ol.proj.get(l.projection);
+ // FIXME if the projection def is not available...
+ if (source_opt.projection) {
+ var projectionExtent =
+ source_opt.projection.getExtent();
+ tilegrid_opt.origin =
+ ol.extent.getTopLeft(projectionExtent);
+ }
+ }
+ // FIXME deprecated?
+ // if (l.units) {
+ // opt.units = l.units;
+ // }
+ if (l.resolutions) {
+ tilegrid_opt.resolutions =
+ l.resolutions.split(',').map(Number);
+ var nbRes = tilegrid_opt.resolutions.length;
+ var matrixIds = new Array(nbRes);
+ for (var i = 0; i < nbRes; i++) {
+ matrixIds[i] = i;
+ }
+ tilegrid_opt.matrixIds = matrixIds;
+ }
+ if (l.max_extent) {
+ var extent = l.max_extent.split(',').map(Number);
+ layer_opt.extent = extent;
+ tilegrid_opt.extent = extent;
+ }
+ if (l.params) {
+ source_opt.dimensions = JSON.parse(l.params);
+ }
+
+ source_opt.tileGrid = new ol.tilegrid.WMTS(tilegrid_opt);
+ layer_opt.source = new ol.source.WMTS(source_opt);
+ out.push(new ol.layer.Tile(layer_opt));
+ } else {
+ switch (l.raster_type) {
+ case "osm":
+ out.push(
+ new ol.layer.Tile({
+ title: l.name,
+ visible: !l.overlay,
+ type: 'base',
+ source: new ol.source.OSM(),
+ })
+ );
+ break;
+ default:
+ var customLayers = this.handleCustomLayers(l);
+ if (customLayers.length) {
+ out = out.concat(customLayers);
+ }
+ break;
+ }
+ }
+ }.bind(this));
+ return out;
+ },
+
+ // To be overridden in geoengine extensions
+ handleCustomLayers: function(layer) {
+ return [];
+ }
+ });
+
+ return BackgroundLayers;
+});
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);
+});
diff --git a/base_geoengine/static/src/js/widgets/geoengine_record.js b/base_geoengine/static/src/js/widgets/geoengine_record.js
new file mode 100644
index 0000000..4f6619e
--- /dev/null
+++ b/base_geoengine/static/src/js/widgets/geoengine_record.js
@@ -0,0 +1,74 @@
+odoo.define('base_geoengine.Record', function (require) {
+ "use strict";
+
+ var field_utils = require('web.field_utils');
+ var session = require('web.session');
+ var Widget = require('web.Widget');
+
+ var fields_registry = require('base_geoengine.template_widgets').registry;
+
+ var GeoengineRecord = Widget.extend({
+ template: 'Geoengine.Record',
+
+ init: function (parent, record, options) {
+ this._super(parent);
+ this.fields = options.fields;
+ this.qweb = options.qweb;
+ this.record = record;
+ this.id = record.id;
+ this.sub_widgets = [];
+ this._initContent();
+ },
+
+ _initContent: function () {
+ this.record = this._transformRecord(this.record);
+ this._renderContent();
+ },
+
+ _transformRecord: function (record) {
+ var new_record = {};
+ _.each(_.extend(_.object(_.keys(this.fields), []), record),
+ function (value, name) {
+ var r = _.clone(this.fields[name] || {});
+ r.raw_value = value;
+ var formatted_value = field_utils.format[r.type](value, r);
+ r.value = formatted_value;
+ new_record[name] = r;
+ }.bind(this)
+ );
+ return new_record;
+ },
+
+ _renderContent: function () {
+ var qweb_context = {
+ record: this.record,
+ widget: this,
+ user_context: session.user_context,
+ formats: formats,
+ };
+ this.content = this.qweb.render('layer-box', qweb_context);
+ },
+
+ start: function () {
+ this._addWidgets();
+ },
+
+ _addWidgets: function () {
+ var self = this;
+ self.$("field").each(function () {
+ var $field = $(this);
+ var field = self.record[$field.attr("name")];
+ var type = $field.attr("widget") || field.type;
+ var FieldWidget = fields_registry.get(type);
+ if (FieldWidget !== undefined) {
+ var widget = new FieldWidget(self, field, $field);
+ widget.replace($field);
+ self.sub_widgets.push(widget);
+ }
+ });
+ },
+
+ });
+
+ return GeoengineRecord;
+});
diff --git a/base_geoengine/static/src/js/widgets/geoengine_template_widgets.js b/base_geoengine/static/src/js/widgets/geoengine_template_widgets.js
new file mode 100644
index 0000000..21d8177
--- /dev/null
+++ b/base_geoengine/static/src/js/widgets/geoengine_template_widgets.js
@@ -0,0 +1,104 @@
+/* ---------------------------------------------------------
+ * Odoo base_geoengine
+ * Author Benjamin Willig 2017 Acsone SA/NV
+ * ---------------------------------------------------------
+ */
+
+odoo.define('base_geoengine.template_widgets', function (require) {
+ "use strict";
+
+ var field_utils = require('web.field_utils');
+ var pyUtils = require('web.py_utils');
+ var Registry = require('web.Registry');
+ var Widget = require('web.Widget');
+
+ /**
+ * Interface to be implemented by geonengine fields.
+ *
+ */
+ var FieldInterface = {
+
+ /**
+ * @constructor
+ * - parent: The widget's parent.
+ * - field: A dictionary giving details about the field, including
+ * the current field's value in the raw_value field.
+ * - $node: The field <field> tag as it appears in the view,
+ * encapsulated in a jQuery object.
+ */
+ init: function (parent, field, $node) {},
+ };
+
+ /**
+ * Abstract class for classes implementing FieldInterface.
+ *
+ * Properties:
+ * - value: useful property to hold the value of the field. By default,
+ * the constructor sets value property.
+ */
+ var AbstractField = Widget.extend(FieldInterface, {
+
+ /**
+ * @constructor
+ * Saves the field and $node parameters and sets the "value" property.
+ */
+ init: function (parent, field, $node) {
+ this._super(parent, field, $node);
+ this.field = field;
+ this.$node = $node;
+ this.options = pyUtils.py_eval(this.$node.attr("options") || '{}');
+ this.format_descriptor = _.extend({}, this.field, {
+ 'widget': this.$node.attr('widget'),
+ });
+ this.set("value", field.raw_value);
+ },
+
+ renderElement: function () {
+ var format = field_utils.format[this.field.type];
+ this.$el.text(format(this.field.raw_value, this.format_descriptor));
+ },
+ });
+
+ var FieldChar = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ var FieldMany2one = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ var FieldFloat = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ var FieldFloatTime = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ var FieldDate = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ var FieldDatetime = AbstractField.extend({
+ tagName: 'span',
+ });
+
+ // TODO: rather use:
+ // var registry = require('web.field_registry');
+ // registry.add(...
+ var fields_registry = new Registry();
+ fields_registry
+ .add('char', FieldChar)
+ .add('many2one', FieldMany2one)
+ .add('float', FieldFloat)
+ .add('float_time', FieldFloatTime)
+ .add('date', FieldDate)
+ .add('datetime', FieldDatetime)
+ ;
+
+ return {
+ AbstractField: AbstractField,
+ registry: fields_registry,
+ };
+
+});
diff --git a/base_geoengine/static/src/js/widgets/geoengine_widgets.js b/base_geoengine/static/src/js/widgets/geoengine_widgets.js
new file mode 100644
index 0000000..bca843c
--- /dev/null
+++ b/base_geoengine/static/src/js/widgets/geoengine_widgets.js
@@ -0,0 +1,586 @@
+/* ---------------------------------------------------------
+ * 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-2016 Camptocamp SA
+ * License in __manifest__.py at root level of the module
+ * ---------------------------------------------------------
+ */
+odoo.define('base_geoengine.geoengine_widgets', function (require) {
+ "use strict";
+
+ var core = require('web.core');
+ var AbstractField = require('web.AbstractField');
+ var geoengine_common = require('base_geoengine.geoengine_common');
+ var BackgroundLayers = require('base_geoengine.BackgroundLayers');
+ var field_registry = require('web.field_registry');
+
+ var FieldGeoEngineEditMap = AbstractField.extend(geoengine_common.GeoengineMixin, { // eslint-disable-line max-len
+ template: 'FieldGeoEngineEditMap',
+
+ geoType: null,
+ map: null,
+ defaultExtent: null,
+ format: null,
+ vectorLayer: null,
+ rasterLayers: null,
+ source: null,
+ features: null,
+ drawControl: null,
+ modifyControl: null,
+ tabListenerInstalled: false,
+ bgLayers: new BackgroundLayers(),
+
+ // --------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ start: function () {
+ var def = this._super();
+
+ // Add a listener on parent tab if it exists in order to refresh
+ // geoengine view we need to trigger it on DOM update for changes
+ // from view to edit mode.
+ core.bus.on('DOM_updated', this, function () {
+ this._addTabListener();
+ }.bind(this));
+
+ return def;
+ },
+
+ // FIXME still used?
+ validate: function () {
+ this.invalid = false;
+ },
+
+ // --------------------------------------------------------------------
+ // Private
+ // --------------------------------------------------------------------
+
+ _createVectorLayer: function () {
+ this.features = new ol.Collection();
+ this.source = new ol.source.Vector({features: this.features});
+ return new ol.layer.Vector({
+ source: this.source,
+ style: new ol.style.Style({
+ fill: new ol.style.Fill({
+ color: '#ee9900',
+ opacity: 0.7,
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#ee9900',
+ width: 3,
+ opacity: 1,
+ }),
+ image: new ol.style.Circle({
+ radius: 7,
+ fill: new ol.style.Fill({
+ color: '#ffcc33',
+ }),
+ }),
+ }),
+ });
+ },
+
+ _createLayers: function (field_infos) {
+ this.vectorLayer = this._createVectorLayer();
+ this.rasterLayers = this.bgLayers.create([
+ field_infos.edit_raster,
+ ]);
+ if (this.rasterLayers.length) {
+ this.rasterLayers[0].isBaseLayer = true;
+ }
+ },
+
+ _addTabListener: function () {
+ if (this.tabListenerInstalled) {
+ return;
+ }
+ var tab = this.$el.closest('div.tab-pane');
+ if (!tab.length) {
+ return;
+ }
+ var tab_link = $('a[href="#' + tab[0].id + '"]')
+ if (!tab_link.length) {
+ return;
+ }
+ tab_link.on('shown.bs.tab', function (e) {
+ this._render();
+ }.bind(this));
+ this.tabListenerInstalled = true;
+ },
+
+ _parseValue: function (value) {
+ return value;
+ },
+
+ _updateMapEmpty: function () {
+ var map_view = this.map.getView();
+ // Default extent
+ if (map_view) {
+ var extent = this.defaultExtent.replace(/\s/g, '').split(',');
+ extent = extent.map(coord => Number(coord));
+ map_view.fit(extent, {maxZoom: this.defaultZoom || 5});
+ }
+ },
+
+ _updateMapZoom: function (zoom) {
+
+ var map_zoom = typeof zoom === 'undefined' ? true : zoom;
+
+ if (this.source) {
+ var extent = this.source.getExtent();
+ var infinite_extent = [
+ Infinity, Infinity, -Infinity, -Infinity,
+ ];
+ if (map_zoom && extent !== infinite_extent) {
+ var map_view = this.map.getView();
+ if (map_view) {
+ map_view.fit(extent, {maxZoom: 15});
+ }
+ }
+ }
+ },
+
+ _setValue: function (value, zoom) {
+
+ this._super(value);
+ this.value = value;
+
+ if (this.map) {
+
+ var ft = new ol.Feature({
+ geometry: new ol.format.GeoJSON().readGeometry(value),
+ labelPoint: new ol.format.GeoJSON().readGeometry(value),
+ });
+ this.source.clear();
+ this.source.addFeature(ft);
+ if (value) {
+ this._updateMapZoom(zoom);
+ } else {
+ this._updateMapEmpty();
+ }
+ }
+ },
+
+ _isTabVisible: function () {
+ var tab = this.$el.closest('div.tab-pane');
+ if (!tab.length) {
+ return false;
+ }
+ return tab.is(":visible");
+ },
+
+ _onUIChange: function () {
+ var value = null;
+ if (this._geometry) {
+ value = this.format.writeGeometry(this._geometry);
+ }
+ this._setValue(value, false);
+ },
+
+ _setupControls: function () {
+
+ /* Add a draw interaction depending on geoType of the field
+ * plus adds a modify interaction to be able to change line
+ * and polygons.
+ * As modify needs to get pointer position on map it requires
+ * the map to be rendered before being created
+ */
+ var handler = null;
+ if (this.geoType === 'POLYGON') {
+ handler = "Polygon";
+ } else if (this.geoType === 'MULTIPOLYGON') {
+ handler = "MultiPolygon";
+ } else if (this.geoType === 'LINESTRING') {
+ handler = "LineString";
+ } else if (this.geoType === 'MULTILINESTRING') {
+ handler = "MultiLineString";
+ } else if (this.geoType === 'POINT') {
+ handler = "Point";
+ } else if (this.geoType === 'MULTIPOINT') {
+ handler = "MultiPoint";
+ } else {
+ // FIXME: unsupported geo type
+ }
+
+ var drawControl = function (options) {
+ ol.interaction.Draw.call(this, options);
+ };
+ ol.inherits(drawControl, ol.interaction.Draw);
+ drawControl.prototype.finishDrawing = function () {
+ this.source_.clear();
+ ol.interaction.Draw.prototype.finishDrawing.call(this);
+ };
+
+ this.drawControl = new drawControl({
+ source: this.source,
+ type: handler,
+ });
+ this.map.addInteraction(this.drawControl);
+ var onchange_geom = function (e) {
+ // Trigger onchanges when drawing is done
+ if (e.type === 'drawend') {
+ this._geometry = e.feature.getGeometry();
+ } else {
+ // Modify end
+ this._geometry = e.features.item(0).getGeometry();
+ }
+ this._onUIChange();
+ }.bind(this);
+ this.drawControl.on('drawend', onchange_geom);
+
+ this.features = this.source.getFeaturesCollection();
+ this.modifyControl = new ol.interaction.Modify({
+ features: this.features,
+ // The SHIFT key must be pressed to delete vertices, so
+ // that new vertices can be drawn at the same position
+ // of existing vertices
+ deleteCondition: function (event) {
+ return ol.events.condition.shiftKeyOnly(event) &&
+ ol.events.condition.singleClick(event);
+ },
+ });
+ this.map.addInteraction(this.modifyControl);
+ this.modifyControl.on('modifyend', onchange_geom);
+
+ var self = this;
+ var ClearMapControl = function (opt_options) {
+ var options = opt_options || {};
+ var button = document.createElement('button');
+ button.innerHTML = '<i class="fa fa-trash"/>';
+ button.addEventListener('click', function () {
+ self.source.clear();
+ self._geometry = null;
+ self._onUIChange();
+ });
+ var element = document.createElement('div');
+ element.className = 'ol-clear ol-unselectable ol-control';
+ element.appendChild(button);
+
+ ol.control.Control.call(this, {
+ element: element,
+ target: options.target,
+ });
+ };
+ ol.inherits(ClearMapControl, ol.control.Control);
+ this.clearmapControl = new ClearMapControl();
+ this.map.addControl(this.clearmapControl);
+ },
+
+ _renderMap: function () {
+ if (!this.map) {
+ var $el = this.$el[0];
+ $($el).css({width: '100%', height: '100%'});
+ this.map = new ol.Map({
+ layers: this.rasterLayers,
+ target: $el,
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 5,
+ }),
+ });
+ this.map.addLayer(this.vectorLayer);
+
+ this.format = new ol.format.GeoJSON({
+ internalProjection: this.map.getView().getProjection(),
+ externalProjection: 'EPSG:' + this.srid,
+ });
+
+ $(document).trigger('FieldGeoEngineEditMap:ready', [this.map]);
+ this._setValue(this.value);
+
+ if (this.mode !== 'readonly' &&
+ !this.get('effective_readonly')) {
+ this._setupControls();
+ this.drawControl.setActive(true);
+ this.modifyControl.setActive(true);
+ this.clearmapControl.element.children[0].disabled = false;
+ }
+ }
+ },
+
+ _render: function () {
+ this._rpc({
+ model: this.model,
+ method: 'get_edit_info_for_geo_column',
+ args: [this.name],
+ }).then(function (result) {
+ this._createLayers(result);
+ this.geoType = result.geo_type;
+ this.projection = result.projection;
+ this.defaultExtent = result.default_extent;
+ this.defaultZoom = result.default_zoom;
+ this.restrictedExtent = result.restricted_extent;
+ this.srid = result.srid;
+ if (this.$el.is(":visible") || this._isTabVisible()) {
+ this._renderMap();
+ }
+ }.bind(this));
+ },
+ });
+
+ // TODO migrate the following widgets
+
+ var FieldGeoPointXY = AbstractField.extend({
+ template: 'FieldGeoPointXY',
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.$input = this.$el.find('input');
+ this.$input.change(this._onUIChange);
+ this.setupFocus(this.$input);
+ },
+
+ get_coords: function () {
+
+ /* Get coordinates and check it has the right format
+ *
+ * @return [x, y]
+ */
+ var x = openerp.web.parse_value(this.$input.eq(0).val(), {
+ type: 'float',
+ });
+ var y = openerp.web.parse_value(this.$input.eq(1).val(), {
+ type: 'float',
+ });
+ return [x, y];
+ },
+
+ make_GeoJSON: function (coords) {
+ return {"type": "Point", "coordinates": coords};
+ },
+
+ _setValue: function (value) {
+ this._super.apply(this, arguments);
+
+ if (value) {
+ var geo_obj = JSON.parse(value);
+ this.$input.eq(0).val(geo_obj.coordinates[0]);
+ this.$input.eq(1).val(geo_obj.coordinates[1]);
+ } else {
+ this.$input.val('');
+ }
+ },
+
+ _onUIChange: function () {
+ var coords = this.get_coords();
+ if (coords[0] && coords[1]) {
+ var json = this.make_GeoJSON(coords);
+ this.value = JSON.stringify(json);
+ } else {
+ this.value = false;
+ }
+ },
+
+ validate: function () {
+ this.invalid = false;
+ try {
+ // Get coords to check if floats
+ var coords = this.get_coords();
+
+ // Make sure the two coordinates are set or None
+ this.invalid = this.required &&
+ (coords[0] === 0 || coords[1] === 0 ) ||
+ coords[0] === false && coords[1] !== false ||
+ coords[0] !== false && coords[1] === false;
+ } catch (e) {
+ this.invalid = true;
+ }
+ },
+
+ update_dom: function () {
+ this._super.apply(this, arguments);
+ this.set_readonly(this.readonly);
+ },
+
+ set_readonly: function () {
+ this.$input.prop('readonly', this.readonly);
+ },
+ });
+
+ var FieldGeoPointXYReadonly = FieldGeoPointXY.extend({
+ template: 'FieldGeoPointXY.readonly',
+
+ _setValue: function (value) {
+ this._super.apply(this, arguments);
+ var show_value = '';
+ if (value) {
+ var geo_obj = JSON.parse(value);
+ show_value = "(" + geo_obj.coordinates[0] + ", " +
+ geo_obj.coordinates[1] + ")";
+ }
+ this.$el.find('div').text(show_value);
+ return show_value;
+ },
+
+ validate: function () {
+ this.invalid = false;
+ },
+ });
+
+ var FieldGeoRect = AbstractField.extend({
+ template: 'FieldGeoRect',
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.$input = this.$el.find('input');
+ this.$input.change(this._onUIChange);
+ this.setupFocus(this.$input);
+ },
+
+ get_coords: function () {
+
+ /* Get coordinates in the input fields
+ *
+ * @return [[x1, y1],[x2, y2]]
+ */
+ var x1 = openerp.web.parse_value(this.$input.eq(0).val(), {
+ type: 'float',
+ });
+ var y1 = openerp.web.parse_value(this.$input.eq(1).val(), {
+ type: 'float',
+ });
+ var x2 = openerp.web.parse_value(this.$input.eq(2).val(), {
+ type: 'float',
+ });
+ var y2 = openerp.web.parse_value(this.$input.eq(3).val(), {
+ type: 'float',
+ });
+
+ return [[x1, y1], [x2, y2]];
+ },
+
+ make_GeoJSON: function (coords) {
+ var p1 = coords[0];
+ var p2 = [coords[0][0], coords[1][1]];
+ var p3 = coords[1];
+ var p4 = [coords[1][0], coords[0][1]];
+ // Create a loop in clockwise
+ var points = [[ p1, p2, p3, p4, p1 ]];
+ return {"type": "Polygon", "coordinates": points};
+ },
+
+ _setValue: function (value) {
+ this._super.apply(this, arguments);
+
+ if (value) {
+ var geo_obj = JSON.parse(value);
+ this.$input.eq(0).val(geo_obj.coordinates[0][0][0]);
+ this.$input.eq(1).val(geo_obj.coordinates[0][0][1]);
+ this.$input.eq(2).val(geo_obj.coordinates[0][2][0]);
+ this.$input.eq(3).val(geo_obj.coordinates[0][2][1]);
+ } else {
+ this.$input.val('');
+ }
+ },
+
+ correct_bounds: function (coords) {
+
+ /* Reverse bounds if the upper right
+ * point is smaller than bottom left
+ *
+ * @return [[x1, y1],[x2, y2]]
+ */
+ var x1 = coords[0][0],
+ y1 = coords[0][1],
+ x2 = coords[1][0],
+ y2 = coords[1][1];
+
+ var minx = Math.min(x1, x2);
+ var maxx = Math.max(x1, x2);
+
+ var miny = Math.min(y1, y2);
+ var maxy = Math.max(y1, y2);
+
+ return [[minx, miny], [maxx, maxy]];
+ },
+
+ _onUIChange: function () {
+ var coords = this.get_coords();
+ if (this.all_are_set(coords)) {
+ coords = this.correct_bounds(coords);
+ var json = this.make_GeoJSON(coords);
+ this.value = JSON.stringify(json);
+ } else {
+ this.value = false;
+ }
+ },
+
+ all_are_set: function (coords) {
+ return coords[0][0] !== false && coords[0][1] !== false &&
+ coords[1][0] !== false && coords[1][1] !== false;
+ },
+
+ none_are_set: function (coords) {
+ return coords[0][0] === false && coords[0][1] === false &&
+ coords[1][0] === false && coords[1][1] === false;
+ },
+
+ validate: function () {
+ this.invalid = false;
+ try {
+ // Get coords to check if floats
+ var coords = this.get_coords();
+
+ // Make sure all the coordinates are set
+ // if not None or if required
+ this.invalid = (this.required ||
+ !this.none_are_set(coords)) &&
+ !this.all_are_set(coords);
+ } catch (e) {
+ this.invalid = true;
+ }
+ },
+
+ update_dom: function () {
+ this._super.apply(this, arguments);
+ this.set_readonly(this.readonly);
+ },
+
+ set_readonly: function () {
+ this.$input.prop('readonly', this.readonly);
+ },
+ });
+
+ var FieldGeoRectReadonly = FieldGeoRect.extend({
+ template: 'FieldGeoRect.readonly',
+
+ _setValue: function (value) {
+ this._super.apply(this, arguments);
+ var show_value = '';
+ if (value) {
+ var geo_obj = JSON.parse(value);
+ show_value = "(" + geo_obj.coordinates[0][0][0] + ", " +
+ geo_obj.coordinates[0][0][1] + "), " +
+ "(" + geo_obj.coordinates[0][2][0] + ", " +
+ geo_obj.coordinates[0][2][1] + ")";
+ }
+ this.$el.find('div').text(show_value);
+ return show_value;
+ },
+
+ validate: function () {
+ this.invalid = false;
+ },
+ });
+
+ field_registry.add('geo_edit_map', FieldGeoEngineEditMap);
+ // .add('geo_point_xy', FieldGeoPointXY)
+ // .add('geo_point_xy', FieldGeoPointXYReadonly)
+ // .add('geo_rect', FieldGeoRect)
+ // .add('geo_rect', FieldGeoRectReadonly);
+
+ return {
+ FieldGeoEngineEditMap: FieldGeoEngineEditMap,
+ // FieldGeoPointXY: FieldGeoPointXY,
+ // FieldGeoPointXYReadonly: FieldGeoPointXYReadonly,
+ // FieldGeoRect: FieldGeoRect,
+ // FieldGeoRectReadonly: FieldGeoRectReadonly,
+ };
+
+});
diff --git a/base_geoengine/static/src/xml/geoengine.xml b/base_geoengine/static/src/xml/geoengine.xml
new file mode 100644
index 0000000..b796d7b
--- /dev/null
+++ b/base_geoengine/static/src/xml/geoengine.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <div t-name="GeoengineView" t-att-id="elem_id"
+ class="container-fluid o_geoengine_container"
+ style="height:100%;position:relative;">
+ <br/>
+ <div id="olmap" class="olmap">
+ <div id="map_infobox" class="map_overlay">
+
+ <div id="map_info_open">
+ <span class="fa fa-edit"/>
+ </div>
+ <div id="map_info_filter_selection">
+ <span class="fa fa-filter">Filter selection</span>
+ </div>
+ <div id="map_info"/>
+ </div>
+
+ <div class="layer-popup ol-popup">
+ <a href="javascript: void(0)" class="ol-popup-edit fa fa-edit"/>
+ <a href="javascript: void(0)" class="popup-closer ol-popup-closer"/>
+ <div class="popup-content">
+ <div/>
+ </div>
+ </div>
+
+ <div id="map_legend" class="ol-control"/>
+ </div>
+ </div>
+
+ <t t-name="Geoengine.Record">
+ <div>
+ <t t-raw="widget.content"/>
+ </div>
+ </t>
+
+ <t t-name="FieldGeoPointXY">
+ <table cellpadding="0" cellspacing="0" border="0" width="100%">
+ <tr>
+ <td align="center" valign="middle">
+ X:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ <td align="center" valign="middle">
+ Y:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ </tr>
+ </table>
+ </t>
+
+ <t t-name="FieldGeoPointXY.readonly">
+ <div t-att-id="widget.name"
+ t-attf-class="field_#{widget.widget} #{_(['geo_point_xy']).contains(widget.widget) ? 'oe-number' : ''}">
+ </div>
+ </t>
+
+ <t t-name="FieldGeoRect">
+ <table cellpadding="0" cellspacing="0" border="0" width="100%">
+ <tr>
+ <td colspan="4" valign="middle">
+ Bottom-Left
+ </td>
+ </tr>
+ <tr>
+ <td align="center" valign="middle">
+ X:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ <td align="center" valign="middle">
+ Y:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="4" valign="middle">
+ Up-Right
+ </td>
+ </tr>
+ <tr>
+ <td align="center" valign="middle">
+ X:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ <td align="center" valign="middle">
+ Y:
+ </td>
+ <td width="47%" valign="top">
+ <t t-call="FieldChar"/>
+ </td>
+ </tr>
+ </table>
+ </t>
+ <t t-name="FieldGeoRect.readonly">
+ <div t-att-id="widget.name"
+ t-attf-class="field_#{widget.widget} #{_(['geo_point_xy']).contains(widget.widget) ? 'oe-number' : ''}">
+ </div>
+ </t>
+
+ <t t-name="FieldGeoEngineEditMap">
+ <div t-att-id="widget.name" t-attf-class="field_#{widget.widget}"
+ border="1"/>
+ </t>
+
+</templates>