diff options
Diffstat (limited to 'base_geoengine/static/src/js')
10 files changed, 2164 insertions, 0 deletions
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, + }; + +}); |
