diff --git a/web_view_google_map/views/google_places_template.xml b/web_view_google_map/views/google_places_template.xml index 2c8c54fbe..4385a22f6 100755 --- a/web_view_google_map/views/google_places_template.xml +++ b/web_view_google_map/views/google_places_template.xml @@ -4,12 +4,13 @@ + - + - + diff --git a/web_widget_google_map_drawing/README.rst b/web_widget_google_map_drawing/README.rst new file mode 100644 index 000000000..38929e877 --- /dev/null +++ b/web_widget_google_map_drawing/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/web_widget_google_map_drawing/__init__.py b/web_widget_google_map_drawing/__init__.py new file mode 100644 index 000000000..bd4813aba --- /dev/null +++ b/web_widget_google_map_drawing/__init__.py @@ -0,0 +1,3 @@ +# Copyright Yopi Angi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models diff --git a/web_widget_google_map_drawing/__manifest__.py b/web_widget_google_map_drawing/__manifest__.py new file mode 100644 index 000000000..e898b09b0 --- /dev/null +++ b/web_widget_google_map_drawing/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright Yopi Angi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + 'name': 'Google Map View Drawing Mixin', + 'summary': 'Add drawing tools to Google Map view in Odoo', + 'version': '12.0.1.0.0', + 'author': 'Yopi Angi, Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/geospatial', + 'license': 'AGPL-3', + 'category': 'Extra Tools', + 'depends': [ + 'web_view_google_map', + ], + 'images': ['static/description/thumbnails.png'], + 'data': [ + 'data/google_maps_library.xml', + 'views/template.xml', + 'views/res_config_settings_views.xml', + 'security/ir.model.access.csv', + ], + 'qweb': ['static/src/xml/drawing.xml'], + 'installable': True, + 'maintainers': [ + 'gityopie', + 'brian10048', + ], +} diff --git a/web_widget_google_map_drawing/data/google_maps_library.xml b/web_widget_google_map_drawing/data/google_maps_library.xml new file mode 100644 index 000000000..ad5622ae6 --- /dev/null +++ b/web_widget_google_map_drawing/data/google_maps_library.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/web_widget_google_map_drawing/models/__init__.py b/web_widget_google_map_drawing/models/__init__.py new file mode 100644 index 000000000..2922941e3 --- /dev/null +++ b/web_widget_google_map_drawing/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright Yopi Angi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import res_config_settings +from . import drawing_mixin diff --git a/web_widget_google_map_drawing/models/drawing_mixin.py b/web_widget_google_map_drawing/models/drawing_mixin.py new file mode 100644 index 000000000..94a5fa868 --- /dev/null +++ b/web_widget_google_map_drawing/models/drawing_mixin.py @@ -0,0 +1,26 @@ +# Copyright Yopi Angi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models +from odoo.tools import safe_eval + + +class GoogleMapDrawingShapeMixin(models.AbstractModel): + _name = 'google.map.drawing.shape.mixin' + _description = 'Google Maps Shape Mixin' + _rec_name = 'shape_name' + + shape_name = fields.Char(string='Name') + shape_area = fields.Float(string='Area') + shape_radius = fields.Float(string='Radius') + shape_description = fields.Text(string='Description') + shape_type = fields.Selection([ + ('circle', 'Circle'), + ('polygon', 'Polygon'), + ('rectangle', 'Rectangle')], + string='Type', default='polygon', required=True) + shape_paths = fields.Text(string='Paths') + + @api.multi + def decode_shape_paths(self): + self.ensure_one() + return safe_eval(self.shape_paths) diff --git a/web_widget_google_map_drawing/models/res_config_settings.py b/web_widget_google_map_drawing/models/res_config_settings.py new file mode 100644 index 000000000..e5415ef95 --- /dev/null +++ b/web_widget_google_map_drawing/models/res_config_settings.py @@ -0,0 +1,42 @@ +# Copyright Yopi Angi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, models, fields + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + google_maps_drawing = fields.Boolean(string='Drawing') + + @api.multi + def set_values(self): + super(ResConfigSettings, self).set_values() + ICPSudo = self.env['ir.config_parameter'].sudo() + libraries = self._set_google_maps_drawing() + ICPSudo.set_param('google.maps_libraries', libraries) + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + lib_drawing = self._get_google_maps_drawing() + res['google_maps_drawing'] = lib_drawing + return res + + @api.model + def _get_google_maps_drawing(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_libraries = ICPSudo.get_param( + 'google.maps_libraries', default='') + libraries = google_maps_libraries.split(',') + return 'drawing' in libraries + + @api.multi + def _set_google_maps_drawing(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_libraries = ICPSudo.get_param( + 'google.maps_libraries', default='') + libraries = google_maps_libraries.split(',') + if self.google_maps_drawing: + libraries.append('drawing') + result = ','.join(libraries) + return result diff --git a/web_widget_google_map_drawing/readme/CONTRIBUTORS.rst b/web_widget_google_map_drawing/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..16ee788cf --- /dev/null +++ b/web_widget_google_map_drawing/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Yopi Angi +* Brian McMaster diff --git a/web_widget_google_map_drawing/readme/DESCRIPTION.rst b/web_widget_google_map_drawing/readme/DESCRIPTION.rst new file mode 100644 index 000000000..77ef84d49 --- /dev/null +++ b/web_widget_google_map_drawing/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +This module extends the Google Map web view in Odoo to allow users to draw +polygons, rectangles, and circles on the map. + +[![Demo](https://i.ytimg.com/vi/DDUFT6XP8AU/2.jpg)](https://youtu.be/DDUFT6XP8AU "Demo") + +More information about the drawing tools can be found [here](https://developers.google.com/maps/documentation/javascript/examples/drawing-tools) + +This module will support three kind of shapes: +- [Rectangle](https://developers.google.com/maps/documentation/javascript/examples/rectangle-simple) +- [Polygon](https://developers.google.com/maps/documentation/javascript/examples/polygon-simple) +- [Circle](https://developers.google.com/maps/documentation/javascript/examples/polygon-simple) diff --git a/web_widget_google_map_drawing/readme/USAGE.rst b/web_widget_google_map_drawing/readme/USAGE.rst new file mode 100644 index 000000000..9e337b484 --- /dev/null +++ b/web_widget_google_map_drawing/readme/USAGE.rst @@ -0,0 +1,75 @@ +This module provides an extendable framework and cannot be used on its own. +The following provides information on how to implement it for your own module. + +## Drawing Mixin + +To ease the implementation of this feature, a mixin class has been defined that you can use in your model + +.. code-block:: python + + class GoogleMapDrawingShapeMixin(models.AbstractModel): + _name = 'google.map.drawing.shape.mixin' + _description = 'Google Maps Shape Mixin' + _rec_name = 'shape_name' + + shape_name = fields.Char(string='Name') + shape_area = fields.Float(string='Area') + shape_radius = fields.Float(string='Radius') + shape_description = fields.Text(string='Description') + shape_type = fields.Selection([ + ('circle', 'Circle'), + ('polygon', 'Polygon'), + ('rectangle', 'Rectangle')], + string='Type', default='polygon', required=True) + shape_paths = fields.Text(string='Paths') + + @api.multi + def decode_shape_paths(self): + self.ensure_one() + return safe_eval(self.shape_paths) + +How to use the widget + +.. code-block:: xml + + + +How to load shape(s) on `map` view + +.. code-block:: xml + + + view.res.partner.area.map + res.partner.area + + + + + + + + + + + + + + + + + + + + + Area: + + + Radius: + + + + + + + + diff --git a/web_widget_google_map_drawing/security/ir.model.access.csv b/web_widget_google_map_drawing/security/ir.model.access.csv new file mode 100644 index 000000000..a4f08d353 --- /dev/null +++ b/web_widget_google_map_drawing/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_google_map_drawing_shape_mixin,access_google_map_drawing_shape_mixin,model_google_map_drawing_shape_mixin,base.group_user,1,1,1,1 diff --git a/web_widget_google_map_drawing/static/description/thumbnails.png b/web_widget_google_map_drawing/static/description/thumbnails.png new file mode 100644 index 000000000..5008cfad6 Binary files /dev/null and b/web_widget_google_map_drawing/static/description/thumbnails.png differ diff --git a/web_widget_google_map_drawing/static/src/js/widget/field_gmaps_drawing.js b/web_widget_google_map_drawing/static/src/js/widget/field_gmaps_drawing.js new file mode 100644 index 000000000..493bd0387 --- /dev/null +++ b/web_widget_google_map_drawing/static/src/js/widget/field_gmaps_drawing.js @@ -0,0 +1,386 @@ +odoo.define('widget_google_maps_drawing.FieldMapDrawingShape', function (require) { + 'use strict'; + + var core = require('web.core'); + var BasicFields = require('web.basic_fields'); + var _t = core._t; + var qweb = core.qweb; + + var FieldMapDrawingShape = BasicFields.InputField.extend({ + class: 'o_field_text o_field_map_drawing', + tagName: 'div', + template: 'WidgetDrawing.Map', + supportedFieldTypes: ['text'], + init: function () { + this._super.apply(this, arguments); + this.selectedShapes = {}; + this.editModeColor = '#006ee5'; + }, + /** + * Override + */ + start: function () { + this._initMap(); + return this._super(); + }, + /** + * @private + * Initialize map + */ + _initMap: function () { + this.gmap = new google.maps.Map(this.$('.o_map_view').get(0), { + center: { + lat: -34.397, + lng: 150.644 + }, + mapTypeId: google.maps.MapTypeId.ROADMAP, + zoom: 3, + minZoom: 3, + maxZoom: 20, + fullscreenControl: true, + mapTypeControl: true, + gestureHandling: 'cooperative', + mapTypeControlOptions: { + mapTypeIds: ['satellite', 'hybrid', 'terrain'], + style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR, + } + }); + }, + /** + * @private + * Initialize drawing manager + */ + _initDrawing: function (mode) { + var drawingOptions = { + fillColor: this.editModeColor, + strokeWeight: 0, + fillOpacity: 0.45, + editable: true + }; + var polylineOptions = { + strokeColor: this.editModeColor, + strokeWeight: 2 + }; + var circleOptions = { + fillColor: this.editModeColor, + fillOpacity: 0.45, + strokeWeight: 0, + editable: true, + zIndex: 1 + }; + this.gmapDrawingManager = new google.maps.drawing.DrawingManager({ + drawingControl: true, + drawingControlOptions: { + position: google.maps.ControlPosition.BOTTOM_CENTER, + drawingModes: ['circle', 'polygon', 'rectangle'] + }, + map: this.gmap, + polylineOptions: { + editable: true + }, + rectangleOptions: drawingOptions, + polygonOptions: drawingOptions, + circleOptions: circleOptions, + polylineOptions: polylineOptions + }); + google.maps.event.addListener(this.gmapDrawingManager, 'overlaycomplete', this._overlayCompleted.bind(this)); + google.maps.event.addListener(this.gmapDrawingManager, 'drawingmode_changed', this._clearSelectedShape.bind(this)); + google.maps.event.addListener(this.gmap, 'click', this._clearSelectedShape.bind(this)); + this._loadDrawingActionButton(); + this._loadShapeExisted(); + }, + /** + * @private + * Draw existed shape + */ + _loadShapeExisted: function () { + var value = this._formatValue(this.value); + if (value) { + value = JSON.parse(value); + if (value.type === 'polygon') { + var polygon = this._drawPolygon(value.options); + polygon.setOptions({ + editable: true, + strokeColor: this.editModeColor, + fillColor: this.editModeColor, + }); + var selectedShape = polygon; + selectedShape.type = 'polygon'; + this._setSelectedShape(selectedShape); + google.maps.event.addListener(selectedShape, 'dblclick', this._setSelectedShape.bind(this, selectedShape)); + // event to handle when user editing polygon + google.maps.event.addListener(polygon.getPath(), 'set_at', this._onPolygonCommit.bind(this)); + google.maps.event.addListener(polygon.getPath(), 'insert_at', this._onPolygonCommit.bind(this)); + } else if (value.type === 'rectangle') { + var rectangle = this._drawRectangle(value.options); + rectangle.setOptions({ + editable: true, + draggable: true, + strokeColor: this.editModeColor, + fillColor: this.editModeColor, + }); + var selectedShape = rectangle; + selectedShape.type = 'rectangle'; + this._setSelectedShape(selectedShape); + // event to handle when user editing rectangle + google.maps.event.addListener(selectedShape, 'click', this._setSelectedShape.bind(this, selectedShape)); + google.maps.event.addListener(rectangle, 'bounds_changed', this._onRectangleCommit.bind(this)); + } else if (value.type === 'circle') { + var circle = this._drawCircle(value.options); + circle.setOptions({ + editable: true, + draggable: true, + strokeColor: this.editModeColor, + fillColor: this.editModeColor, + }); + var selectedShape = circle; + selectedShape.type = 'circle'; + this._setSelectedShape(selectedShape); + google.maps.event.addListener(selectedShape, 'click', this._setSelectedShape.bind(this, selectedShape)); + // event to handle when user editing circle + google.maps.event.addListener(circle, 'radius_changed', this._onCircleCommit.bind(this)); + google.maps.event.addListener(circle, 'center_changed', this._onCircleCommit.bind(this)); + } + } + }, + /** + * @private + * Draw polygon + */ + _drawPolygon: function (options) { + var polygon = new google.maps.Polygon({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + editable: false, + map: this.gmap, + }); + polygon.setOptions(options); + this._mapCenterMap(polygon.getPath()); + return polygon; + }, + /** + * @private + * Draw rectangle + */ + _drawRectangle: function (options) { + var rectangle = new google.maps.Rectangle({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + map: this.gmap, + editable: false, + draggable: false + }); + rectangle.setOptions(options); + this._mapCenterMap(false, rectangle.getBounds()); + return rectangle; + }, + /** + * @private + * Draw circle + */ + _drawCircle: function (options) { + var circle = new google.maps.Circle({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + map: this.gmap, + editable: false, + draggable: false + }); + circle.setOptions(options); + this._mapCenterMap(false, circle.getBounds()); + return circle; + }, + /** + * @override + */ + _renderReadonly: function () { + var value = this._formatValue(this.value); + if (value) { + var shapePath = JSON.parse(value); + var shapeOptions = shapePath.options; + if (shapePath.type === 'polygon') { + this._drawPolygon(shapeOptions); + } else if (shapePath.type === 'rectangle') { + this._drawRectangle(shapeOptions); + } else if (shapePath.type === 'circle') { + this._drawCircle(shapeOptions); + } + } + }, + /** + * @override + */ + _renderEdit: function () { + this._super.apply(this, arguments); + this._initDrawing(); + }, + /** + * @private + * Callback function when overlay is completed + */ + _overlayCompleted: function (event) { + // Switch back to non-drawing mode after drawing a shape. + this.gmapDrawingManager.setDrawingMode(null); + + var newShape = event.overlay; + var uniqueId = new Date().getTime(); + + newShape.type = event.type; + newShape._drawId = uniqueId; + + this.selectedShapes[uniqueId] = newShape; + google.maps.event.addListener(newShape, 'click', this._setSelectedShape.bind(this, newShape)); + this._setSelectedShape(newShape); + this._commitShapeDraw(); + }, + /** + * @private + * Set selected shape + */ + _setSelectedShape: function (newShape) { + this.selectedShape = newShape; + this.selectedShape.setEditable(true); + }, + /** + * @private + * Clear selected shape + */ + _clearSelectedShape: function () { + if (this.selectedShape) { + this.selectedShape.setEditable(false); + this.selectedShape = null; + } + }, + /** + * @private + * Load action buttons into the map + */ + _loadDrawingActionButton: function () { + if (this.$btnDrawingClear === undefined) { + this.$btnDrawingClear = $(qweb.render('WidgetDrawing.BtnDelete', { + widget: this + })); + this.gmap.controls[google.maps.ControlPosition.BOTTOM_CENTER].push(this.$btnDrawingClear.get(0)); + this.$btnDrawingClear.on('click', this._deleteSelectedShaped.bind(this)); + } + }, + /** + * @private + * Polygon + */ + _onPolygonCommit: function () { + var paths = this.selectedShape.getPath(); + var area = google.maps.geometry.spherical.computeArea(paths); + var paths_latLng = []; + paths.forEach(function (item) { + paths_latLng.push({ + 'lat': item.lat(), + 'lng': item.lng() + }); + }); + var values = { + 'shape_type': this.selectedShape.type, + 'shape_area': area, + }; + var shape_paths = { + 'type': this.selectedShape.type, + 'options': { + paths: paths_latLng + } + }; + this._onTriggerUp(values, shape_paths); + }, + _onRectangleCommit: function () { + var values = { + 'shape_type': this.selectedShape.type, + }; + var bounds = this.selectedShape.getBounds(); + var directions = bounds.toJSON(); + var shape_paths = { + 'type': this.selectedShape.type, + 'options': { + 'bounds': directions + } + }; + this._onTriggerUp(values, shape_paths); + }, + _onCircleCommit: function () { + var radius = this.selectedShape.getRadius(); + var center = this.selectedShape.getCenter(); + var values = { + 'shape_type': this.selectedShape.type, + 'shape_radius': radius + }; + var shape_paths = { + 'type': this.selectedShape.type, + 'options': { + radius: radius, + center: { + 'lat': center.lat(), + 'lng': center.lng() + } + } + }; + this._onTriggerUp(values, shape_paths); + }, + _onTriggerUp: function (values, shape_paths) { + var values = values || {}; + var shape_paths = shape_paths || {}; + values['shape_paths'] = JSON.stringify(shape_paths); + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: values, + viewType: this.viewType + }); + }, + /** + * @private + */ + _commitShapeDraw: function () { + if (Object.keys(this.selectedShapes).length > 1) { + this.do_warn(_t('Only one shape is allowed!')); + return; + } + if (this.selectedShape.type === 'polygon') { + this._onPolygonCommit(); + } else if (this.selectedShape.type === 'rectangle') { + this._onRectangleCommit(); + } else if (this.selectedShape.type === 'circle') { + this._onCircleCommit(); + } + }, + _deleteSelectedShaped: function (event) { + event.preventDefault(); + if (this.selectedShape) { + delete this.selectedShapes[this.selectedShape._drawId]; + this.selectedShape.setMap(null); + this._onTriggerUp(); + } + }, + _mapCenterMap: function (paths, bounds) { + paths = paths || []; + bounds = bounds || false; + var mapBounds = new google.maps.LatLngBounds(); + if (paths.length > 0) { + paths.forEach(function (item) { + mapBounds.extend({ lat: item.lat(), lng: item.lng() }); + }); + } else if (bounds) { + mapBounds.union(bounds); + } + this.gmap.fitBounds(mapBounds); + }, + }); + + return FieldMapDrawingShape; + +}); diff --git a/web_widget_google_map_drawing/static/src/js/widget/fields_registry.js b/web_widget_google_map_drawing/static/src/js/widget/fields_registry.js new file mode 100644 index 000000000..53437e884 --- /dev/null +++ b/web_widget_google_map_drawing/static/src/js/widget/fields_registry.js @@ -0,0 +1,11 @@ +odoo.define('web_google_maps_drawing.FieldsRegistry', function (require) { + 'use strict'; + + var registry = require('web.field_registry'); + var FieldMapDrawingShape = require( + 'widget_google_maps_drawing.FieldMapDrawingShape' + ); + + registry.add('map_drawing_shape', FieldMapDrawingShape); + +}); diff --git a/web_widget_google_map_drawing/static/src/xml/drawing.xml b/web_widget_google_map_drawing/static/src/xml/drawing.xml new file mode 100644 index 000000000..9a46732f7 --- /dev/null +++ b/web_widget_google_map_drawing/static/src/xml/drawing.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web_widget_google_map_drawing/views/res_config_settings_views.xml b/web_widget_google_map_drawing/views/res_config_settings_views.xml new file mode 100644 index 000000000..d0a17ae0c --- /dev/null +++ b/web_widget_google_map_drawing/views/res_config_settings_views.xml @@ -0,0 +1,23 @@ + + + res.config.settings.view.form.inherit.web_google_maps_drawing + res.config.settings + + + + + + + + + + + Drawing provides a graphical interface for users to draw polygons, rectangles, polylines, circles, and markers on the map. + Consult the Drawing library documentation for more information. + + + + + + + diff --git a/web_widget_google_map_drawing/views/template.xml b/web_widget_google_map_drawing/views/template.xml new file mode 100644 index 000000000..a142a98a5 --- /dev/null +++ b/web_widget_google_map_drawing/views/template.xml @@ -0,0 +1,8 @@ + + + + + + + +