diff --git a/ext/custom-addons/web_google_maps/.gitignore b/ext/custom-addons/web_google_maps/.gitignore new file mode 100644 index 00000000..d4a80c86 --- /dev/null +++ b/ext/custom-addons/web_google_maps/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.pyc + diff --git a/ext/custom-addons/web_google_maps/README.md b/ext/custom-addons/web_google_maps/README.md new file mode 100644 index 00000000..5232831e --- /dev/null +++ b/ext/custom-addons/web_google_maps/README.md @@ -0,0 +1,289 @@ +Web Google Maps +=============== + +[![Demo](https://i.ytimg.com/vi/2UdG5ILDtiY/3.jpg)](https://youtu.be/5hvAubXgUnc "Demo") + + +This module contains three new features: + - New view type and mode `map` + - New widget `gplaces_address_autocomplete` + - New widget `gplaces_autocomplete` + + +# Map view `"map"` +Basically, this new view `map` will integrate Google Maps into Odoo. +Enable you to display `res.partner` geolocation on map or any model contains geolocation. +This feature will work seamlessly with Odoo means you can search your partner location using Odoo search feature. + +There are five available attributes that you can customize + - `lat` : an attritube to tell the map the latitude field on the object __[mandatory]__ + - `lng` : an attritute to tell the map the longitude field on the object __[mandatory]__ + - `color` : an attribute to modify marker color (optional) any given color will set all markers color __[optional]__. + - `colors` : work like attribute `color` but more configurable (you can set marker color depends on it's value) this attribute works similar to `colors` of tree view on Odoo 9.0 __[optional]__ + - `library` : an attribute to tell map which map that will be loaded __[mandatory]__. + This options has two values: + 1. `geometry` + 2. `drawing` + +How to create the view? +Example +> + + + view.res.partner.map + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+
    +
  • + +
  • +
  • + +
  • +
  • + at +
  • +
  • + +
  • +
  • + +
  • +
  • + + , +
  • +
  • + +
  • +
+ +
+ + + + + + + + + ... + form + tree,form,map + ... + + + +The view looks familiar? +Yes, you're right. +The marker infowindow will use `kanban-box` kanban card style. + + +### How to setup color for marker on map? +There are two attributes: + - `colors` + - `color` + +Example: +> + + + ... + + + + + ... + + + +# New widget `"gplaces_address_autocomplete"` + +New widget to integrate [Place Autocomplete Address Form](https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete-addressform) in Odoo. +The widget has four options that can be modify: + - `component_form` + - `fillfields` + - `lat` + - `lng` + +### Component form `component_form` +Is an option used to modify which value you want to take from an objects returned by the geocoder. +Full documentation about Google component types can be found [here](https://developers.google.com/maps/documentation/geocoding/intro#Types) +By default this option are configured like the following value +> + { + 'street_number': 'long_name', + 'route': 'long_name', + 'intersection': 'short_name', + 'political': 'short_name', + 'country': 'short_name', + 'administrative_area_level_1': 'short_name', + 'administrative_area_level_2': 'short_name', + 'administrative_area_level_3': 'short_name', + 'administrative_area_level_4': 'short_name', + 'administrative_area_level_5': 'short_name', + 'colloquial_area': 'short_name', + 'locality': 'short_name', + 'ward': 'short_name', + 'sublocality_level_1': 'short_name', + 'sublocality_level_2': 'short_name', + 'sublocality_level_3': 'short_name', + 'sublocality_level_5': 'short_name', + 'neighborhood': 'short_name', + 'premise': 'short_name', + 'postal_code': 'short_name', + 'natural_feature': 'short_name', + 'airport': 'short_name', + 'park': 'short_name', + 'point_of_interest': 'long_name' + } + +This configuration can be modify into view field definition. +Example: +> + + ... + + ... + + ... + + + + +### Fill fields `fillfields` +Is an option that will be influenced by `gplaces_address_autocomplete` widget. +This options should contains known `fields` that you want the widget to fulfill a value for each given field automatically. +A field can contains one or multiple elements of component form +By default this options are configured like the following +> + { + 'street': ['street_number', 'route'], + 'street2': ['administrative_area_level_3', 'administrative_area_level_4', 'administrative_area_level_5'], + 'city': ['locality', 'administrative_area_level_2'], + 'zip': 'postal_code', + 'state_id': 'administrative_area_level_1', + 'country_id': 'country', + } + + +This configuration can be modify into view field definition as well +Example: +> + + ... + + ... + + ... + + + +### Latitude `lat` and Longitude `lng` +This options tell the widget the fields geolocation, in order to have this fields filled automatically. + + +# New widget `"gplaces_autocomplete"` + +New widget to integrate [Place Autocomplete](https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete) in Odoo. +This widget have similar configuration to `gplaces_address_autocomplete`. + +### Component form `component_form` ### +Same configuration of `gplaces_address_autocomplete` component form + +### Fill fields `fillfields` +This configuration works similar to `gplaces_address_autocomplete`. +By default this options are configured like following value: +> + { + general: { + name: 'name', + website: 'website', + phone: ['international_phone_number', 'formatted_phone_number'] + }, + geolocation: { + partner_latitude: 'latitude', + partner_longitude: 'longitude' + }, + address: { + street: ['street_number', 'route'], + street2: ['administrative_area_level_3', 'administrative_area_level_4', 'administrative_area_level_5'], + city: ['locality', 'administrative_area_level_2'], + zip: 'postal_code', + state_id: 'administrative_area_level_1', + country_id: 'country' + } + }; + +# Technical +This module will install `base_setup` and `base_geolocalize`. +*I recommend you to setup __Google Maps Key API__ and add it into Odoo `Settings > General` Settings when you installed this module* + +*__List of Google APIs & services required in order to make all features works__* +- Geocoding API +- Maps JavaScript API +- Places API for Web + + +The goal of this module is to bring the power of Google Maps into Odoo +This module has tested on Odoo Version 11.0c + +[![ko-fi](https://www.ko-fi.com/img/donate_sm.png)](https://ko-fi.com/P5P4FOM0) +*if you want to support me to keep this project maintained. Thanks :)* \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/__init__.py b/ext/custom-addons/web_google_maps/__init__.py new file mode 100644 index 00000000..c2929c5a --- /dev/null +++ b/ext/custom-addons/web_google_maps/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 +from . import models +from . import controllers +from .hooks import uninstall_hook diff --git a/ext/custom-addons/web_google_maps/__manifest__.py b/ext/custom-addons/web_google_maps/__manifest__.py new file mode 100644 index 00000000..319a168f --- /dev/null +++ b/ext/custom-addons/web_google_maps/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Web Google Maps', + 'version': '11.0.1.0.5', + 'author': 'Yopi Angi', + 'license': 'AGPL-3', + 'maintainer': 'Yopi Angi', + 'support': 'yopiangi@gmail.com', + 'category': 'Extra Tools', + 'description': """ +Web Google Map and google places autocomplete address form +========================================================== + +This module brings three features: +1. Allows user to view all partners addresses on google maps. +2. Enabled google places autocomplete address form into partner +form view, it provide autocomplete feature when typing address of partner +""", + 'depends': [ + 'base_setup', + 'base_geolocalize', + ], + 'website': '', + 'data': [ + 'data/google_maps_libraries.xml', + 'views/google_places_template.xml', + 'views/res_partner.xml', + 'views/res_config.xml' + ], + 'demo': [], + 'images': ['static/description/thumbnails.png'], + 'qweb': ['static/src/xml/widget_places.xml'], + 'installable': True, + 'uninstall_hook': 'uninstall_hook', +} diff --git a/ext/custom-addons/web_google_maps/controllers/__init__.py b/ext/custom-addons/web_google_maps/controllers/__init__.py new file mode 100644 index 00000000..757b12a1 --- /dev/null +++ b/ext/custom-addons/web_google_maps/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/ext/custom-addons/web_google_maps/controllers/main.py b/ext/custom-addons/web_google_maps/controllers/main.py new file mode 100644 index 00000000..1836c06a --- /dev/null +++ b/ext/custom-addons/web_google_maps/controllers/main.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from odoo import http + + +class Main(http.Controller): + + @http.route('/web/map_theme', type='json', auth='user') + def map_theme(self): + ICP = http.request.env['ir.config_parameter'].sudo() + theme = ICP.get_param('google.maps_theme', default='default') + res = {'theme': theme} + return res diff --git a/ext/custom-addons/web_google_maps/data/google_maps_libraries.xml b/ext/custom-addons/web_google_maps/data/google_maps_libraries.xml new file mode 100644 index 00000000..5dedab80 --- /dev/null +++ b/ext/custom-addons/web_google_maps/data/google_maps_libraries.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/ext/custom-addons/web_google_maps/hooks.py b/ext/custom-addons/web_google_maps/hooks.py new file mode 100644 index 00000000..a7526d88 --- /dev/null +++ b/ext/custom-addons/web_google_maps/hooks.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 + +def uninstall_hook(cr, registry): + cr.execute("UPDATE ir_act_window " + "SET view_mode=replace(view_mode, ',map', '')" + "WHERE view_mode LIKE '%,map%';") + cr.execute("UPDATE ir_act_window " + "SET view_mode=replace(view_mode, 'map,', '')" + "WHERE view_mode LIKE '%map,%';") + cr.execute("DELETE FROM ir_act_window " + "WHERE view_mode = 'map';") diff --git a/ext/custom-addons/web_google_maps/i18n/web_google_maps.pot b/ext/custom-addons/web_google_maps/i18n/web_google_maps.pot new file mode 100644 index 00000000..b1423881 --- /dev/null +++ b/ext/custom-addons/web_google_maps/i18n/web_google_maps.pot @@ -0,0 +1,466 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_google_maps +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-09 16:04+0000\n" +"PO-Revision-Date: 2018-10-09 16:04+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Api key" +msgstr "Api key" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Arabic" +msgstr "Arabic" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Aubergine" +msgstr "Aubergine" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Basque" +msgstr "Basque" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Bengali" +msgstr "Bengali" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Bulgarian" +msgstr "Bulgarian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Catalan" +msgstr "Catalan" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Chinese (Simplified)" +msgstr "Chinese (Simplified)" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Chinese (Traditional)" +msgstr "Chinese (Traditional)" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Configure your Google Maps View" +msgstr "Configure your Google Maps View" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Croatian" +msgstr "Croatian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Czech" +msgstr "Czech" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Danish" +msgstr "Danish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Dark" +msgstr "Dark" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Default" +msgstr "Default" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Dutch" +msgstr "Dutch" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "English" +msgstr "English" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "English (Australian)" +msgstr "English (Australian)" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "English (Great Britain)" +msgstr "English (Great Britain)" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Farsi" +msgstr "Farsi" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Filipino" +msgstr "Filipino" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Finnish" +msgstr "Finnish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "French" +msgstr "French" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Galician" +msgstr "Galician" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_geometry +msgid "Geometry" +msgstr "Geometry" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Geometry includes utility functions for calculating scalar geometric values (such as distance and area) on the surface of the earth. \n" +" Consult the" +msgstr "Geometry includes utility functions for calculating scalar geometric values (such as distance and area) on the surface of the earth. \n" +" Consult the" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Geometry library documentation" +msgstr "Geometry library documentation" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "German" +msgstr "German" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_lang_localization +msgid "Google Maps Language Localization" +msgstr "Google Maps Language Localization" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Google Maps Libraries" +msgstr "Google Maps Libraries" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_region_localization +msgid "Google Maps Region Localization" +msgstr "Google Maps Region Localization" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Google Maps View" +msgstr "Google Maps View" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_view_api_key +msgid "Google Maps View Api Key" +msgstr "Google Maps View Api Key" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Greek" +msgstr "Greek" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Gujarati" +msgstr "Gujarati" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Hebrew" +msgstr "Hebrew" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Hindi" +msgstr "Hindi" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Hungarian" +msgstr "Hungarian" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "If you set the language of the map, it's important to consider setting the region too. This helps ensure that your application complies with local laws." +msgstr "If you set the language of the map, it's important to consider setting the region too. This helps ensure that your application complies with local laws." + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Indonesian" +msgstr "Indonesian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Italian" +msgstr "Italian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Japanese" +msgstr "Japanese" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Kannada" +msgstr "Kannada" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Korean" +msgstr "Korean" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Language" +msgstr "Language" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Latvian" +msgstr "Latvian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Lithuanian" +msgstr "Lithuanian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Malayalam" +msgstr "Malayalam" + +#. module: web_google_maps +#. openerp-web +#: code:addons/web_google_maps/static/src/js/view/map/map_view.js:15 +#: model:ir.ui.view,arch_db:web_google_maps.view_res_partner_map +#, python-format +msgid "Map" +msgstr "Map" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_theme +msgid "Map theme" +msgstr "Map theme" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Marathi" +msgstr "Marathi" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Night" +msgstr "Night" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Norwegian" +msgstr "Norwegian" + +#. module: web_google_maps +#: model:ir.model.fields,field_description:web_google_maps.field_res_config_settings_google_maps_places +msgid "Places" +msgstr "Places" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Places enables your application to search for places such as establishments, geographic locations, or prominent points of interest, within a defined area. \n" +" Consult the" +msgstr "Places enables your application to search for places such as establishments, geographic locations, or prominent points of interest, within a defined area. \n" +" Consult the" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Places library documentation" +msgstr "Places library documentation" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Polish" +msgstr "Polish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Portuguese" +msgstr "Portuguese" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Portuguese (Brazil)" +msgstr "Portuguese (Brazil)" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Portuguese (Portugal)" +msgstr "Portuguese (Portugal)" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Region" +msgstr "Region" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Retro" +msgstr "Retro" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Romanian" +msgstr "Romanian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Russian" +msgstr "Russian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Serbian" +msgstr "Serbian" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Set API keys and map localization" +msgstr "Set API keys and map localization" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_theme:0 +msgid "Silver" +msgstr "Silver" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Slovak" +msgstr "Slovak" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Slovenian" +msgstr "Slovenian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Spanish" +msgstr "Spanish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Swedish" +msgstr "Swedish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Tagalog" +msgstr "Tagalog" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Tamil" +msgstr "Tamil" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Telugu" +msgstr "Telugu" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Thai" +msgstr "Thai" + +#. module: web_google_maps +#. openerp-web +#: code:addons/web_google_maps/static/src/js/widgets/gplaces_autocomplete.js:244 +#: code:addons/web_google_maps/static/src/js/widgets/gplaces_autocomplete.js:352 +#, python-format +msgid "The following fields are invalid:" +msgstr "The following fields are invalid:" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Theme" +msgstr "Theme" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Turkish" +msgstr "Turkish" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Ukrainian" +msgstr "Ukrainian" + +#. module: web_google_maps +#: selection:res.config.settings,google_maps_lang_localization:0 +msgid "Vietnamese" +msgstr "Vietnamese" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "Visit the" +msgstr "Visit the" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "about Localizing the Map" +msgstr "about Localizing the Map" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_res_partner_map +msgid "at" +msgstr "at" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "for more information." +msgstr "for more information." + +#. module: web_google_maps +#: model:ir.model,name:web_google_maps.model_ir_actions_act_window_view +msgid "ir.actions.act_window.view" +msgstr "ir.actions.act_window.view" + +#. module: web_google_maps +#: model:ir.model,name:web_google_maps.model_ir_ui_view +msgid "ir.ui.view" +msgstr "ir.ui.view" + +#. module: web_google_maps +#: model:ir.ui.view,arch_db:web_google_maps.view_web_google_maps_config_settings +msgid "page" +msgstr "page" + +#. module: web_google_maps +#: model:ir.model,name:web_google_maps.model_res_config_settings +msgid "res.config.settings" +msgstr "res.config.settings" diff --git a/ext/custom-addons/web_google_maps/models/__init__.py b/ext/custom-addons/web_google_maps/models/__init__.py new file mode 100644 index 00000000..54362baf --- /dev/null +++ b/ext/custom-addons/web_google_maps/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 +from . import ir_act_window_view +from . import ir_ui_view +from . import res_config diff --git a/ext/custom-addons/web_google_maps/models/ir_act_window_view.py b/ext/custom-addons/web_google_maps/models/ir_act_window_view.py new file mode 100644 index 00000000..23979925 --- /dev/null +++ b/ext/custom-addons/web_google_maps/models/ir_act_window_view.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = 'ir.actions.act_window.view' + + view_mode = fields.Selection(selection_add=[('map', 'Map')]) diff --git a/ext/custom-addons/web_google_maps/models/ir_ui_view.py b/ext/custom-addons/web_google_maps/models/ir_ui_view.py new file mode 100644 index 00000000..9939bbe1 --- /dev/null +++ b/ext/custom-addons/web_google_maps/models/ir_ui_view.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 +from odoo import api, fields, models + + +class IrUiView(models.Model): + _inherit = 'ir.ui.view' + + type = fields.Selection(selection_add=[('map', 'Map')]) diff --git a/ext/custom-addons/web_google_maps/models/res_config.py b/ext/custom-addons/web_google_maps/models/res_config.py new file mode 100644 index 00000000..f9566794 --- /dev/null +++ b/ext/custom-addons/web_google_maps/models/res_config.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# License AGPL-3 +from odoo import api, fields, models + +GMAPS_LANG_LOCALIZATION = [ + ('ar', 'Arabic'), + ('bg', 'Bulgarian'), + ('bn', 'Bengali'), + ('ca', 'Catalan'), + ('cs', 'Czech'), + ('da', 'Danish'), + ('de', 'German'), + ('el', 'Greek'), + ('en', 'English'), + ('en-AU', 'English (Australian)'), + ('en-GB', 'English (Great Britain)'), + ('es', 'Spanish'), + ('eu', 'Basque'), + ('eu', 'Basque'), + ('fa', 'Farsi'), + ('fi', 'Finnish'), + ('fil', 'Filipino'), + ('fr', 'French'), + ('gl', 'Galician'), + ('gu', 'Gujarati'), + ('hi', 'Hindi'), + ('hr', 'Croatian'), + ('hu', 'Hungarian'), + ('id', 'Indonesian'), + ('it', 'Italian'), + ('iw', 'Hebrew'), + ('ja', 'Japanese'), + ('kn', 'Kannada'), + ('ko', 'Korean'), + ('lt', 'Lithuanian'), + ('lv', 'Latvian'), + ('ml', 'Malayalam'), + ('mr', 'Marathi'), + ('nl', 'Dutch'), + ('no', 'Norwegian'), + ('pl', 'Polish'), + ('pt', 'Portuguese'), + ('pt-BR', 'Portuguese (Brazil)'), + ('pt-PT', 'Portuguese (Portugal)'), + ('ro', 'Romanian'), + ('ru', 'Russian'), + ('sk', 'Slovak'), + ('sl', 'Slovenian'), + ('sr', 'Serbian'), + ('sv', 'Swedish'), + ('ta', 'Tamil'), + ('te', 'Telugu'), + ('th', 'Thai'), + ('tl', 'Tagalog'), + ('tr', 'Turkish'), + ('uk', 'Ukrainian'), + ('vi', 'Vietnamese'), + ('zh-CN', 'Chinese (Simplified)'), + ('zh-TW', 'Chinese (Traditional)'), +] + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + @api.model + def get_region_selection(self): + country_ids = self.env['res.country'].search([]) + values = [(country.code, country.name) for country in country_ids] + return values + + google_maps_view_api_key = fields.Char(string='Google Maps View Api Key') + google_maps_lang_localization = fields.Selection( + selection=GMAPS_LANG_LOCALIZATION, + string='Google Maps Language Localization') + google_maps_region_localization = fields.Selection( + selection=get_region_selection, + string='Google Maps Region Localization') + google_maps_theme = fields.Selection( + selection=[('default', 'Default'), + ('aubergine', 'Aubergine'), + ('night', 'Night'), + ('dark', 'Dark'), + ('retro', 'Retro'), + ('silver', 'Silver')], + string='Map theme') + google_maps_places = fields.Boolean(string='Places', default=True) + google_maps_geometry = fields.Boolean(string='Geometry', default=True) + + @api.onchange('google_maps_lang_localization') + def onchange_lang_localization(self): + if not self.google_maps_lang_localization: + self.google_maps_region_localization = '' + + @api.multi + def set_values(self): + super(ResConfigSettings, self).set_values() + ICPSudo = self.env['ir.config_parameter'].sudo() + lang_localization = self._set_google_maps_lang_localization() + region_localization = self._set_google_maps_region_localization() + + lib_places = self._set_google_maps_places() + lib_geometry = self._set_google_maps_geometry() + + active_libraries = '%s,%s' % (lib_geometry, lib_places) + + ICPSudo.set_param('google.api_key_geocode', + self.google_maps_view_api_key) + ICPSudo.set_param('google.lang_localization', + lang_localization) + ICPSudo.set_param('google.region_localization', + region_localization) + ICPSudo.set_param('google.maps_theme', self.google_maps_theme) + ICPSudo.set_param('google.maps_libraries', active_libraries) + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + ICPSudo = self.env['ir.config_parameter'].sudo() + + lang_localization = self._get_google_maps_lang_localization() + region_localization = self._get_google_maps_region_localization() + + lib_places = self._get_google_maps_places() + lib_geometry = self._get_google_maps_geometry() + + res.update({ + 'google_maps_view_api_key': ICPSudo.get_param( + 'google.api_key_geocode', default=''), + 'google_maps_lang_localization': lang_localization, + 'google_maps_region_localization': region_localization, + 'google_maps_theme': ICPSudo.get_param( + 'google.maps_theme', default='default'), + 'google_maps_places': lib_places, + 'google_maps_geometry': lib_geometry + }) + return res + + @api.multi + def _set_google_maps_lang_localization(self): + if self.google_maps_lang_localization: + lang_localization = '&language=%s' % \ + self.google_maps_lang_localization + else: + lang_localization = '' + + return lang_localization + + @api.model + def _get_google_maps_lang_localization(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_lang = ICPSudo.get_param( + 'google.lang_localization', default='') + val = google_maps_lang.split('=') + lang = val and val[-1] or '' + return lang + + @api.multi + def _set_google_maps_region_localization(self): + if self.google_maps_region_localization: + region_localization = '®ion=%s' % \ + self.google_maps_region_localization + else: + region_localization = '' + + return region_localization + + @api.model + def _get_google_maps_region_localization(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_region = ICPSudo.get_param( + 'google.region_localization', default='') + val = google_maps_region.split('=') + region = val and val[-1] or '' + return region + + @api.model + def _get_google_maps_geometry(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_libraries = ICPSudo.get_param( + 'google.maps_libraries', default='') + libraries = google_maps_libraries.split(',') + return 'geometry' in libraries + + @api.multi + def _set_google_maps_geometry(self): + return 'geometry' if self.google_maps_geometry else '' + + @api.model + def _get_google_maps_places(self): + ICPSudo = self.env['ir.config_parameter'].sudo() + google_maps_libraries = ICPSudo.get_param( + 'google.maps_libraries', default='') + libraries = google_maps_libraries.split(',') + return 'places' in libraries + + @api.multi + def _set_google_maps_places(self): + return 'places' if self.google_maps_places else '' diff --git a/ext/custom-addons/web_google_maps/static/description/create_new_partner_in_map.png b/ext/custom-addons/web_google_maps/static/description/create_new_partner_in_map.png new file mode 100644 index 00000000..55ab658c Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/create_new_partner_in_map.png differ diff --git a/ext/custom-addons/web_google_maps/static/description/icon.png b/ext/custom-addons/web_google_maps/static/description/icon.png new file mode 100644 index 00000000..e07fdc17 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/icon.png differ diff --git a/ext/custom-addons/web_google_maps/static/description/index.html b/ext/custom-addons/web_google_maps/static/description/index.html new file mode 100644 index 00000000..8b54ab18 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/description/index.html @@ -0,0 +1,77 @@ +
+
+

Integrate Google Maps into Odoo

+

Show all your partners location on Google Maps

+
+ + +
+
+

This module brings four new features:

+
    +
  • New view map allows user to view all partners addresses on google maps.
  • +
  • + New widget gplaces_address_autocomplete, enabled Google places autocomplete address form into partner form view,
    + provide autocomplete feature when you typing an address of partner (or any field using this widget) +
  • +
  • + New widget gplaces_autocomplete, enabled Google places autocomplete into partner form view,
    + provide autocomplete feature when typing partner name (or any field using this widget) +
  • +
  • Map Localization
  • +
+
+
+
+ +
+
+

New Widget Google Place Autocomplete Address Form

+
+ +
+
+

Provide autocomplete feature when typing an address

+
+
+
+ +
+
+

New Widget Google Place Autocomplete

+
+ +
+
+

Provide autocomplete feature when typing partner's name

+
+
+
+ +
+
+
+

Bug Tracker

+

Bugs are tracked on + GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted + it first, help us smashing it by providing a detailed and welcomed feedback.

+
+
+

Contributors

+ +
+
+

Maintainer

+

This module is maintained by myself, if you are interested to contribute please let me know

+ + ko-fi + +

Thank you

+
+
+
diff --git a/ext/custom-addons/web_google_maps/static/description/maps.png b/ext/custom-addons/web_google_maps/static/description/maps.png new file mode 100644 index 00000000..e40e5748 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/maps.png differ diff --git a/ext/custom-addons/web_google_maps/static/description/route_info.png b/ext/custom-addons/web_google_maps/static/description/route_info.png new file mode 100644 index 00000000..25c364c4 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/route_info.png differ diff --git a/ext/custom-addons/web_google_maps/static/description/thumbnails.png b/ext/custom-addons/web_google_maps/static/description/thumbnails.png new file mode 100644 index 00000000..bb9e32b6 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/thumbnails.png differ diff --git a/ext/custom-addons/web_google_maps/static/description/widget_gplaces_address_form.gif b/ext/custom-addons/web_google_maps/static/description/widget_gplaces_address_form.gif new file mode 100644 index 00000000..d029451d Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/widget_gplaces_address_form.gif differ diff --git a/ext/custom-addons/web_google_maps/static/description/widget_gplaces_autocomplete.gif b/ext/custom-addons/web_google_maps/static/description/widget_gplaces_autocomplete.gif new file mode 100644 index 00000000..0aed7b39 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/description/widget_gplaces_autocomplete.gif differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m1.png b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m1.png new file mode 100644 index 00000000..329ff524 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m1.png differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m2.png b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m2.png new file mode 100644 index 00000000..b999cbcf Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m2.png differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m3.png b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m3.png new file mode 100644 index 00000000..9f30b309 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m3.png differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m4.png b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m4.png new file mode 100644 index 00000000..0d3f8263 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m4.png differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m5.png b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m5.png new file mode 100644 index 00000000..61387d2a Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/lib/markercluster/img/m5.png differ diff --git a/ext/custom-addons/web_google_maps/static/lib/markercluster/markerclusterer.js b/ext/custom-addons/web_google_maps/static/lib/markercluster/markerclusterer.js new file mode 100644 index 00000000..5f08eac6 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/lib/markercluster/markerclusterer.js @@ -0,0 +1,1321 @@ +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/maps/google_maps_api_v3_3.js +// ==/ClosureCompiler== + +/** + * @name MarkerClusterer for Google Maps v3 + * @version version 1.0.3 + * @author Luke Mahe + * @fileoverview + * The library creates and manages per-zoom-level clusters for large amounts of + * markers. + */ + +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * A Marker Clusterer that clusters markers. + * + * @param {google.maps.Map} map The Google map to attach to. + * @param {Array.=} opt_markers Optional markers to add to + * the cluster. + * @param {Object=} opt_options support the following options: + * 'gridSize': (number) The grid size of a cluster in pixels. + * 'maxZoom': (number) The maximum zoom level that a marker can be part of a + * cluster. + * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a + * cluster is to zoom into it. + * 'imagePath': (string) The base URL where the images representing + * clusters will be found. The full URL will be: + * {imagePath}[1-5].{imageExtension} + * Default: '../images/m'. + * 'imageExtension': (string) The suffix for images URL representing + * clusters will be found. See _imagePath_ for details. + * Default: 'png'. + * 'averageCenter': (boolean) Whether the center of each cluster should be + * the average of all markers in the cluster. + * 'minimumClusterSize': (number) The minimum number of markers to be in a + * cluster before the markers are hidden and a count + * is shown. + * 'styles': (object) An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition': (string) The position of the backgound x, y. + * @constructor + * @extends google.maps.OverlayView + */ +function MarkerClusterer(map, opt_markers, opt_options) { + // MarkerClusterer implements google.maps.OverlayView interface. We use the + // extend function to extend MarkerClusterer with google.maps.OverlayView + // because it might not always be available when the code is defined so we + // look for it at the last possible moment. If it doesn't exist now then + // there is no point going ahead :) + this.extend(MarkerClusterer, google.maps.OverlayView); + this.map_ = map; + + /** + * @type {Array.} + * @private + */ + this.markers_ = []; + + /** + * @type {Array.} + */ + this.clusters_ = []; + + this.sizes = [53, 56, 66, 78, 90]; + + /** + * @private + */ + this.styles_ = []; + + /** + * @type {boolean} + * @private + */ + this.ready_ = false; + + var options = opt_options || {}; + + /** + * @type {number} + * @private + */ + this.gridSize_ = options['gridSize'] || 60; + + /** + * @private + */ + this.minClusterSize_ = options['minimumClusterSize'] || 2; + + + /** + * @type {?number} + * @private + */ + this.maxZoom_ = options['maxZoom'] || null; + + this.styles_ = options['styles'] || []; + + /** + * @type {string} + * @private + */ + this.imagePath_ = options['imagePath'] || + this.MARKER_CLUSTER_IMAGE_PATH_; + + /** + * @type {string} + * @private + */ + this.imageExtension_ = options['imageExtension'] || + this.MARKER_CLUSTER_IMAGE_EXTENSION_; + + /** + * @type {boolean} + * @private + */ + this.zoomOnClick_ = true; + + if (options['zoomOnClick'] != undefined) { + this.zoomOnClick_ = options['zoomOnClick']; + } + + /** + * @type {boolean} + * @private + */ + this.averageCenter_ = false; + + if (options['averageCenter'] != undefined) { + this.averageCenter_ = options['averageCenter']; + } + + this.setupStyles_(); + + this.setMap(map); + + /** + * @type {number} + * @private + */ + this.prevZoom_ = this.map_.getZoom(); + + // Add the map event listeners + var that = this; + google.maps.event.addListener(this.map_, 'zoom_changed', function() { + // Determines map type and prevent illegal zoom levels + var zoom = that.map_.getZoom(); + var minZoom = that.map_.minZoom || 0; + var maxZoom = Math.min(that.map_.maxZoom || 100, + that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom); + zoom = Math.min(Math.max(zoom,minZoom),maxZoom); + + if (that.prevZoom_ != zoom) { + that.prevZoom_ = zoom; + that.resetViewport(); + } + }); + + google.maps.event.addListener(this.map_, 'idle', function() { + that.redraw(); + }); + + // Finally, add the markers + if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) { + this.addMarkers(opt_markers, false); + } +} + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m'; + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png'; + + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +MarkerClusterer.prototype.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.onAdd = function() { + this.setReady_(true); +}; + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.draw = function() {}; + +/** + * Sets up the styles object. + * + * @private + */ +MarkerClusterer.prototype.setupStyles_ = function() { + if (this.styles_.length) { + return; + } + + for (var i = 0, size; size = this.sizes[i]; i++) { + this.styles_.push({ + url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_, + height: size, + width: size + }); + } +}; + +/** + * Fit the map to the bounds of the markers in the clusterer. + */ +MarkerClusterer.prototype.fitMapToMarkers = function() { + var markers = this.getMarkers(); + var bounds = new google.maps.LatLngBounds(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + + this.map_.fitBounds(bounds); +}; + + +/** + * Sets the styles. + * + * @param {Object} styles The style to set. + */ +MarkerClusterer.prototype.setStyles = function(styles) { + this.styles_ = styles; +}; + + +/** + * Gets the styles. + * + * @return {Object} The styles object. + */ +MarkerClusterer.prototype.getStyles = function() { + return this.styles_; +}; + + +/** + * Whether zoom on click is set. + * + * @return {boolean} True if zoomOnClick_ is set. + */ +MarkerClusterer.prototype.isZoomOnClick = function() { + return this.zoomOnClick_; +}; + +/** + * Whether average center is set. + * + * @return {boolean} True if averageCenter_ is set. + */ +MarkerClusterer.prototype.isAverageCenter = function() { + return this.averageCenter_; +}; + + +/** + * Returns the array of markers in the clusterer. + * + * @return {Array.} The markers. + */ +MarkerClusterer.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the number of markers in the clusterer + * + * @return {Number} The number of markers. + */ +MarkerClusterer.prototype.getTotalMarkers = function() { + return this.markers_.length; +}; + + +/** + * Sets the max zoom for the clusterer. + * + * @param {number} maxZoom The max zoom level. + */ +MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + this.maxZoom_ = maxZoom; +}; + + +/** + * Gets the max zoom for the clusterer. + * + * @return {number} The max zoom level. + */ +MarkerClusterer.prototype.getMaxZoom = function() { + return this.maxZoom_; +}; + + +/** + * The function for calculating the cluster icon image. + * + * @param {Array.} markers The markers in the clusterer. + * @param {number} numStyles The number of styles available. + * @return {Object} A object properties: 'text' (string) and 'index' (number). + * @private + */ +MarkerClusterer.prototype.calculator_ = function(markers, numStyles) { + var index = 0; + var count = markers.length; + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index + }; +}; + + +/** + * Set the calculator function. + * + * @param {function(Array, number)} calculator The function to set as the + * calculator. The function should return a object properties: + * 'text' (string) and 'index' (number). + * + */ +MarkerClusterer.prototype.setCalculator = function(calculator) { + this.calculator_ = calculator; +}; + + +/** + * Get the calculator function. + * + * @return {function(Array, number)} the calculator function. + */ +MarkerClusterer.prototype.getCalculator = function() { + return this.calculator_; +}; + + +/** + * Add an array of markers to the clusterer. + * + * @param {Array.} markers The markers to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) { + if (markers.length) { + for (var i = 0, marker; marker = markers[i]; i++) { + this.pushMarkerTo_(marker); + } + } else if (Object.keys(markers).length) { + for (var marker in markers) { + this.pushMarkerTo_(markers[marker]); + } + } + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Pushes a marker to the clusterer. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.pushMarkerTo_ = function(marker) { + marker.isAdded = false; + if (marker['draggable']) { + // If the marker is draggable add a listener so we update the clusters on + // the drag end. + var that = this; + google.maps.event.addListener(marker, 'dragend', function() { + marker.isAdded = false; + that.repaint(); + }); + } + this.markers_.push(marker); +}; + + +/** + * Adds a marker to the clusterer and redraws if needed. + * + * @param {google.maps.Marker} marker The marker to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) { + this.pushMarkerTo_(marker); + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Removes a marker and returns true if removed, false if not + * + * @param {google.maps.Marker} marker The marker to remove + * @return {boolean} Whether the marker was removed or not + * @private + */ +MarkerClusterer.prototype.removeMarker_ = function(marker) { + var index = -1; + if (this.markers_.indexOf) { + index = this.markers_.indexOf(marker); + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + index = i; + break; + } + } + } + + if (index == -1) { + // Marker is not in our list of markers. + return false; + } + + marker.setMap(null); + + this.markers_.splice(index, 1); + + return true; +}; + + +/** + * Remove a marker from the cluster. + * + * @param {google.maps.Marker} marker The marker to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + * @return {boolean} True if the marker was removed. + */ +MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) { + var removed = this.removeMarker_(marker); + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } else { + return false; + } +}; + + +/** + * Removes an array of markers from the cluster. + * + * @param {Array.} markers The markers to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + */ +MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) { + // create a local copy of markers if required + // (removeMarker_ modifies the getMarkers() array in place) + var markersCopy = markers === this.getMarkers() ? markers.slice() : markers; + var removed = false; + + for (var i = 0, marker; marker = markersCopy[i]; i++) { + var r = this.removeMarker_(marker); + removed = removed || r; + } + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } +}; + + +/** + * Sets the clusterer's ready state. + * + * @param {boolean} ready The state. + * @private + */ +MarkerClusterer.prototype.setReady_ = function(ready) { + if (!this.ready_) { + this.ready_ = ready; + this.createClusters_(); + } +}; + + +/** + * Returns the number of clusters in the clusterer. + * + * @return {number} The number of clusters. + */ +MarkerClusterer.prototype.getTotalClusters = function() { + return this.clusters_.length; +}; + + +/** + * Returns the google map that the clusterer is associated with. + * + * @return {google.maps.Map} The map. + */ +MarkerClusterer.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Sets the google map that the clusterer is associated with. + * + * @param {google.maps.Map} map The map. + */ +MarkerClusterer.prototype.setMap = function(map) { + this.map_ = map; +}; + + +/** + * Returns the size of the grid. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getGridSize = function() { + return this.gridSize_; +}; + + +/** + * Sets the size of the grid. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setGridSize = function(size) { + this.gridSize_ = size; +}; + + +/** + * Returns the min cluster size. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getMinClusterSize = function() { + return this.minClusterSize_; +}; + +/** + * Sets the min cluster size. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setMinClusterSize = function(size) { + this.minClusterSize_ = size; +}; + + +/** + * Extends a bounds object by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + */ +MarkerClusterer.prototype.getExtendedBounds = function(bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + + +/** + * Determins if a marker is contained in a bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the marker is in the bounds. + * @private + */ +MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) { + return bounds.contains(marker.getPosition()); +}; + + +/** + * Clears all clusters and markers from the clusterer. + */ +MarkerClusterer.prototype.clearMarkers = function() { + this.resetViewport(true); + + // Set the markers a empty array. + this.markers_ = []; +}; + + +/** + * Clears all existing clusters and recreates them. + * @param {boolean} opt_hide To also hide the marker. + */ +MarkerClusterer.prototype.resetViewport = function(opt_hide) { + // Remove all the clusters + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + cluster.remove(); + } + + // Reset the markers to not be added and to be invisible. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.isAdded = false; + if (opt_hide) { + marker.setMap(null); + } + } + + this.clusters_ = []; +}; + +/** + * + */ +MarkerClusterer.prototype.repaint = function() { + var oldClusters = this.clusters_.slice(); + this.clusters_.length = 0; + this.resetViewport(); + this.redraw(); + + // Remove the old clusters. + // Do it in a timeout so the other clusters have been drawn first. + window.setTimeout(function() { + for (var i = 0, cluster; cluster = oldClusters[i]; i++) { + cluster.remove(); + } + }, 0); +}; + + +/** + * Redraws the clusters. + */ +MarkerClusterer.prototype.redraw = function() { + this.createClusters_(); +}; + + +/** + * Calculates the distance between two latlng locations in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @private +*/ +MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) { + if (!p1 || !p2) { + return 0; + } + + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + + +/** + * Add a marker to a cluster, or creates a new cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.addToClosestCluster_ = function(marker) { + var distance = 40000; // Some large number + var clusterToAddTo = null; + var pos = marker.getPosition(); + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + var center = cluster.getCenter(); + if (center) { + var d = this.distanceBetweenPoints_(center, marker.getPosition()); + if (d < distance) { + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { + clusterToAddTo.addMarker(marker); + } else { + var cluster = new Cluster(this); + cluster.addMarker(marker); + this.clusters_.push(cluster); + } +}; + + +/** + * Creates the clusters. + * + * @private + */ +MarkerClusterer.prototype.createClusters_ = function() { + if (!this.ready_) { + return; + } + + // Get our current map view bounds. + // Create a new bounds object so we don't affect the map. + var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast()); + var bounds = this.getExtendedBounds(mapBounds); + + for (var i = 0, marker; marker = this.markers_[i]; i++) { + if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { + this.addToClosestCluster_(marker); + } + } +}; + + +/** + * A cluster that contains markers. + * + * @param {MarkerClusterer} markerClusterer The markerclusterer that this + * cluster is associated with. + * @constructor + * @ignore + */ +function Cluster(markerClusterer) { + this.markerClusterer_ = markerClusterer; + this.map_ = markerClusterer.getMap(); + this.gridSize_ = markerClusterer.getGridSize(); + this.minClusterSize_ = markerClusterer.getMinClusterSize(); + this.averageCenter_ = markerClusterer.isAverageCenter(); + this.center_ = null; + this.markers_ = []; + this.bounds_ = null; + this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(), + markerClusterer.getGridSize()); +} + +/** + * Determins if a marker is already added to the cluster. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker is already added. + */ +Cluster.prototype.isMarkerAlreadyAdded = function(marker) { + if (this.markers_.indexOf) { + return this.markers_.indexOf(marker) != -1; + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + return true; + } + } + } + return false; +}; + + +/** + * Add a marker the cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @return {boolean} True if the marker was added. + */ +Cluster.prototype.addMarker = function(marker) { + if (this.isMarkerAlreadyAdded(marker)) { + return false; + } + + if (!this.center_) { + this.center_ = marker.getPosition(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.markers_.length + 1; + var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l; + var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + marker.isAdded = true; + this.markers_.push(marker); + + var len = this.markers_.length; + if (len < this.minClusterSize_ && marker.getMap() != this.map_) { + // Min cluster size not reached so show the marker. + marker.setMap(this.map_); + } + + if (len == this.minClusterSize_) { + // Hide the markers that were showing. + for (var i = 0; i < len; i++) { + this.markers_[i].setMap(null); + } + } + + if (len >= this.minClusterSize_) { + marker.setMap(null); + } + + this.updateIcon(); + return true; +}; + + +/** + * Returns the marker clusterer that the cluster is associated with. + * + * @return {MarkerClusterer} The associated marker clusterer. + */ +Cluster.prototype.getMarkerClusterer = function() { + return this.markerClusterer_; +}; + + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + */ +Cluster.prototype.getBounds = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + var markers = this.getMarkers(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + return bounds; +}; + + +/** + * Removes the cluster + */ +Cluster.prototype.remove = function() { + this.clusterIcon_.remove(); + this.markers_.length = 0; + delete this.markers_; +}; + + +/** + * Returns the number of markers in the cluster. + * + * @return {number} The number of markers in the cluster. + */ +Cluster.prototype.getSize = function() { + return this.markers_.length; +}; + + +/** + * Returns a list of the markers in the cluster. + * + * @return {Array.} The markers in the cluster. + */ +Cluster.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {google.maps.LatLng} The cluster center. + */ +Cluster.prototype.getCenter = function() { + return this.center_; +}; + + +/** + * Calculated the extended bounds of the cluster with the grid. + * + * @private + */ +Cluster.prototype.calculateBounds_ = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); +}; + + +/** + * Determines if a marker lies in the clusters bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker lies in the bounds. + */ +Cluster.prototype.isMarkerInClusterBounds = function(marker) { + return this.bounds_.contains(marker.getPosition()); +}; + + +/** + * Returns the map that the cluster is associated with. + * + * @return {google.maps.Map} The map. + */ +Cluster.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Updates the cluster icon + */ +Cluster.prototype.updateIcon = function() { + var zoom = this.map_.getZoom(); + var mz = this.markerClusterer_.getMaxZoom(); + + if (mz && zoom > mz) { + // The zoom is greater than our max zoom so show all the markers in cluster. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.setMap(this.map_); + } + return; + } + + if (this.markers_.length < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.markerClusterer_.getStyles().length; + var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.setSums(sums); + this.clusterIcon_.show(); +}; + + +/** + * A cluster icon + * + * @param {Cluster} cluster The cluster to be associated with. + * @param {Object} styles An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition: (string) The background postition x, y. + * @param {number=} opt_padding Optional padding to apply to the cluster icon. + * @constructor + * @extends google.maps.OverlayView + * @ignore + */ +function ClusterIcon(cluster, styles, opt_padding) { + cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); + + this.styles_ = styles; + this.padding_ = opt_padding || 0; + this.cluster_ = cluster; + this.center_ = null; + this.map_ = cluster.getMap(); + this.div_ = null; + this.sums_ = null; + this.visible_ = false; + + this.setMap(this.map_); +} + + +/** + * Triggers the clusterclick event and zoom's if the option is set. + */ +ClusterIcon.prototype.triggerClusterClick = function() { + var markerClusterer = this.cluster_.getMarkerClusterer(); + + // Trigger the clusterclick event. + google.maps.event.trigger(markerClusterer.map_, 'clusterclick', this.cluster_); + + if (markerClusterer.isZoomOnClick()) { + // Zoom into the cluster. + this.map_.fitBounds(this.cluster_.getBounds()); + } +}; + + +/** + * Adding the cluster icon to the dom. + * @ignore + */ +ClusterIcon.prototype.onAdd = function() { + this.div_ = document.createElement('DIV'); + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.innerHTML = this.sums_.text; + } + + var panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(this.div_); + + var that = this; + google.maps.event.addDomListener(this.div_, 'click', function() { + that.triggerClusterClick(); + }); +}; + + +/** + * Returns the position to place the div dending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + * @private + */ +ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + return pos; +}; + + +/** + * Draw the icon. + * @ignore + */ +ClusterIcon.prototype.draw = function() { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + 'px'; + this.div_.style.left = pos.x + 'px'; + this.div_.style.zIndex = google.maps.Marker.MAX_ZINDEX + 1; + } +}; + + +/** + * Hide the icon. + */ +ClusterIcon.prototype.hide = function() { + if (this.div_) { + this.div_.style.display = 'none'; + } + this.visible_ = false; +}; + + +/** + * Position and show the icon. + */ +ClusterIcon.prototype.show = function() { + if (this.div_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.style.display = ''; + } + this.visible_ = true; +}; + + +/** + * Remove the icon from the map + */ +ClusterIcon.prototype.remove = function() { + this.setMap(null); +}; + + +/** + * Implementation of the onRemove interface. + * @ignore + */ +ClusterIcon.prototype.onRemove = function() { + if (this.div_ && this.div_.parentNode) { + this.hide(); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; + + +/** + * Set the sums of the icon. + * + * @param {Object} sums The sums containing: + * 'text': (string) The text to display in the icon. + * 'index': (number) The style index of the icon. + */ +ClusterIcon.prototype.setSums = function(sums) { + this.sums_ = sums; + this.text_ = sums.text; + this.index_ = sums.index; + if (this.div_) { + this.div_.innerHTML = sums.text; + } + + this.useStyle(); +}; + + +/** + * Sets the icon to the the styles. + */ +ClusterIcon.prototype.useStyle = function() { + var index = Math.max(0, this.sums_.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style['url']; + this.height_ = style['height']; + this.width_ = style['width']; + this.textColor_ = style['textColor']; + this.anchor_ = style['anchor']; + this.textSize_ = style['textSize']; + this.backgroundPosition_ = style['backgroundPosition']; +}; + + +/** + * Sets the center of the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function(center) { + this.center_ = center; +}; + + +/** + * Create the css text based on the position of the icon. + * + * @param {google.maps.Point} pos The position. + * @return {string} The css style text. + */ +ClusterIcon.prototype.createCss = function(pos) { + var style = []; + style.push('background-image:url(' + this.url_ + ');'); + var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0'; + style.push('background-position:' + backgroundPosition + ';'); + + if (typeof this.anchor_ === 'object') { + if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 && + this.anchor_[0] < this.height_) { + style.push('height:' + (this.height_ - this.anchor_[0]) + + 'px; padding-top:' + this.anchor_[0] + 'px;'); + } else { + style.push('height:' + this.height_ + 'px; line-height:' + this.height_ + + 'px;'); + } + if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 && + this.anchor_[1] < this.width_) { + style.push('width:' + (this.width_ - this.anchor_[1]) + + 'px; padding-left:' + this.anchor_[1] + 'px;'); + } else { + style.push('width:' + this.width_ + 'px; text-align:center;'); + } + } else { + style.push('height:' + this.height_ + 'px; line-height:' + + this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;'); + } + + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + var txtSize = this.textSize_ ? this.textSize_ : 11; + + style.push('cursor:pointer; top:' + pos.y + 'px; left:' + + pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' + + txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return style.join(''); +}; + + +// Export Symbols for Closure +// If you are not going to compile with closure then you can remove the +// code below. +var window = window || {}; +window['MarkerClusterer'] = MarkerClusterer; +MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker; +MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers; +MarkerClusterer.prototype['clearMarkers'] = + MarkerClusterer.prototype.clearMarkers; +MarkerClusterer.prototype['fitMapToMarkers'] = + MarkerClusterer.prototype.fitMapToMarkers; +MarkerClusterer.prototype['getCalculator'] = + MarkerClusterer.prototype.getCalculator; +MarkerClusterer.prototype['getGridSize'] = + MarkerClusterer.prototype.getGridSize; +MarkerClusterer.prototype['getExtendedBounds'] = + MarkerClusterer.prototype.getExtendedBounds; +MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap; +MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers; +MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom; +MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles; +MarkerClusterer.prototype['getTotalClusters'] = + MarkerClusterer.prototype.getTotalClusters; +MarkerClusterer.prototype['getTotalMarkers'] = + MarkerClusterer.prototype.getTotalMarkers; +MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw; +MarkerClusterer.prototype['removeMarker'] = + MarkerClusterer.prototype.removeMarker; +MarkerClusterer.prototype['removeMarkers'] = + MarkerClusterer.prototype.removeMarkers; +MarkerClusterer.prototype['resetViewport'] = + MarkerClusterer.prototype.resetViewport; +MarkerClusterer.prototype['repaint'] = + MarkerClusterer.prototype.repaint; +MarkerClusterer.prototype['setCalculator'] = + MarkerClusterer.prototype.setCalculator; +MarkerClusterer.prototype['setGridSize'] = + MarkerClusterer.prototype.setGridSize; +MarkerClusterer.prototype['setMaxZoom'] = + MarkerClusterer.prototype.setMaxZoom; +MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd; +MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw; + +Cluster.prototype['getCenter'] = Cluster.prototype.getCenter; +Cluster.prototype['getSize'] = Cluster.prototype.getSize; +Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers; + +ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd; +ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw; +ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove; + +Object.keys = Object.keys || function(o) { + var result = []; + for(var name in o) { + if (o.hasOwnProperty(name)) + result.push(name); + } + return result; +}; + +if (typeof module == 'object') { + module.exports = MarkerClusterer; +} diff --git a/ext/custom-addons/web_google_maps/static/src/css/google_places_widget.css b/ext/custom-addons/web_google_maps/static/src/css/google_places_widget.css new file mode 100644 index 00000000..61b5d3c3 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/css/google_places_widget.css @@ -0,0 +1,155 @@ +/* https://github.com/twbs/bootstrap/issues/4160 */ +/* put google places autocomplete dropdown results above the bootstrap modal 1050 zindex. */ +.pac-container { + z-index: 1051 !important; +} + +.o_map { + height: 100%; + width: 100%; +} + +.o_map .o_map_view { + height: 100%; + width: 100%; +} + +.o_map .o_map_sidenav { + left: 0; + top: 0; + bottom: 0; + max-width: 400px; + width: 400px; + position: absolute; + z-index: 1; + background-color: #f5f5f5; + overflow-x: hidden; + transition: 0.5s; + transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); +} + +.o_map .o_map_sidenav.whiteframe { + box-shadow: 0 2px 4px -1px rgba(0, 0, 0, .2), 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12); +} + +.o_map .o_map_sidenav.closed { + transform: translate3d(-100%, 0, 0); + -webkit-transform: translate3d(-100%, 0, 0); +} + +.o_map .o_map_sidenav .sidenav-body .panel { + border: none; +} + +.o_act_window .o_map .o_map_view, +.o_act_window .o_map .o_map_sidenav { + min-height: 400px; + height: 400px; +} + +.o_map .o_map_sidenav .controls, +.o_map .o_map_sidenav .layers { + text-align: center; +} + +.o_map .o_map_sidenav .controls span.active, +.o_map .o_map_sidenav .layers span.active { + color: #7c7bad; + font-weight: bold; +} + +.o_map .o_map_sidenav .controls span img, +.o_map .o_map_sidenav .layers span img { + padding-left: 5px; + padding-right: 10px; +} + +.o_map_control .btn_map_control { + margin-top: 10px; + margin-left: 10px; + cursor: pointer; + transition: margin-left .5s; + line-height: 20px; + padding: 4.5px 10px; + background: #ffffff; + border-radius: 2px; + box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px; +} + +.o_map_control .btn_map_control.opened { + margin-left: 410px; +} + +.o_map_routes_window { + min-width: 100px; + border-radius: 2px; + font-size: 14px; + margin-left: 15px; + opacity: 0.8; + background: #ffffff; + padding: 5px; +} + +.o_map_redirect_google { + cursor: pointer; + text-align: center; + width: 25px; + height: 25px; + margin-right: 15px; + background-color: #ffffff; + border-radius: 2px; + line-height: 25px; +} + +.o_map_places_control .pac-card #pac-container #pac-button { + padding-top: 5px; + text-align: right; +} + +.o_map_places_control .pac-card #pac-container #pac-result { + border-bottom: 1px solid #ccc; +} + +.o_map_places_control .pac-card .pac-controls { + padding: 0 0 10px 0; + text-align: center; +} + +.o_map_places_control .pac-card .pac-controls label { + font-size: 13px; + font-weight: 300; +} + +.o_map_places_control .pac-card #pac-input { + font-size: 15px; + font-weight: 300; + text-overflow: ellipsis; + height: 35px; +} + +.o_map_places_control .pac-card #pac-input:focus { + border-color: #7c7bad; +} + +.o_map_place_result input#place-input-name { + background-color: #efefef; + font-size: 15px; + font-weight: 300; + text-overflow: ellipsis; + height: 35px; +} + +.o_map_place_result ul { + list-style-type: none; + margin-left: -30px; +} + +.o_map_place_result ul li i { + padding-right: 5px; +} + +.o_control_panel .o_map_buttons_view>button:first-child { + float: left; + margin-right: 4px; +} diff --git a/ext/custom-addons/web_google_maps/static/src/img/layer_bike.png b/ext/custom-addons/web_google_maps/static/src/img/layer_bike.png new file mode 100644 index 00000000..a1210bba Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/layer_bike.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/layer_traffic.png b/ext/custom-addons/web_google_maps/static/src/img/layer_traffic.png new file mode 100644 index 00000000..d6a236f3 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/layer_traffic.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/layer_transit.png b/ext/custom-addons/web_google_maps/static/src/img/layer_transit.png new file mode 100644 index 00000000..0f77a4ed Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/layer_transit.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/black.png b/ext/custom-addons/web_google_maps/static/src/img/markers/black.png new file mode 100644 index 00000000..23a0250b Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/black.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/blue.png b/ext/custom-addons/web_google_maps/static/src/img/markers/blue.png new file mode 100644 index 00000000..fbeb0cc0 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/blue.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/brown.png b/ext/custom-addons/web_google_maps/static/src/img/markers/brown.png new file mode 100644 index 00000000..6de8d9a8 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/brown.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/cyan.png b/ext/custom-addons/web_google_maps/static/src/img/markers/cyan.png new file mode 100644 index 00000000..5cb9b996 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/cyan.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/green.png b/ext/custom-addons/web_google_maps/static/src/img/markers/green.png new file mode 100644 index 00000000..ff666a2d Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/green.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/grey.png b/ext/custom-addons/web_google_maps/static/src/img/markers/grey.png new file mode 100644 index 00000000..df3360d6 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/grey.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/light-green.png b/ext/custom-addons/web_google_maps/static/src/img/markers/light-green.png new file mode 100644 index 00000000..465d9e54 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/light-green.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/magenta.png b/ext/custom-addons/web_google_maps/static/src/img/markers/magenta.png new file mode 100644 index 00000000..6e0038b9 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/magenta.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/orange.png b/ext/custom-addons/web_google_maps/static/src/img/markers/orange.png new file mode 100644 index 00000000..bd64101d Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/orange.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/pink.png b/ext/custom-addons/web_google_maps/static/src/img/markers/pink.png new file mode 100644 index 00000000..466b7c35 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/pink.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/purple.png b/ext/custom-addons/web_google_maps/static/src/img/markers/purple.png new file mode 100644 index 00000000..3fc502ca Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/purple.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/red.png b/ext/custom-addons/web_google_maps/static/src/img/markers/red.png new file mode 100644 index 00000000..553f8c48 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/red.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/white.png b/ext/custom-addons/web_google_maps/static/src/img/markers/white.png new file mode 100644 index 00000000..cc6fa8a7 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/white.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/markers/yellow.png b/ext/custom-addons/web_google_maps/static/src/img/markers/yellow.png new file mode 100644 index 00000000..e2ad98c4 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/markers/yellow.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/mode_bicycling.png b/ext/custom-addons/web_google_maps/static/src/img/mode_bicycling.png new file mode 100644 index 00000000..a1210bba Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/mode_bicycling.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/mode_driving.png b/ext/custom-addons/web_google_maps/static/src/img/mode_driving.png new file mode 100644 index 00000000..f98af8d2 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/mode_driving.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/mode_transit.png b/ext/custom-addons/web_google_maps/static/src/img/mode_transit.png new file mode 100644 index 00000000..0f77a4ed Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/mode_transit.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/img/mode_walking.png b/ext/custom-addons/web_google_maps/static/src/img/mode_walking.png new file mode 100644 index 00000000..fd71f608 Binary files /dev/null and b/ext/custom-addons/web_google_maps/static/src/img/mode_walking.png differ diff --git a/ext/custom-addons/web_google_maps/static/src/js/fields/relational_fields.js b/ext/custom-addons/web_google_maps/static/src/js/fields/relational_fields.js new file mode 100644 index 00000000..d82ca0c5 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/fields/relational_fields.js @@ -0,0 +1,61 @@ +odoo.define('web_google_maps.relational_fields', function (require) { + + var core = require('web.core'); + var relational_fields = require('web.relational_fields'); + var MapRenderer = require('web_google_maps.MapRenderer'); + + var qweb = core.qweb; + + relational_fields.FieldOne2Many.include({ + _render: function () { + if (!this.view || this.renderer) { + return this._super(); + } + var arch = this.view.arch; + var viewType; + if (arch.tag == 'map') { + viewType = 'map'; + var record_options = { + editable: true, + deletable: true, + read_only_mode: this.isReadonly + } + this.renderer = new MapRenderer(this, this.value, { + arch: arch, + record_options: record_options, + viewType: viewType, + fieldLat: arch.attrs.lat, + fieldLng: arch.attrs.lng, + markerColor: arch.attrs.color, + mapLibrary: arch.attrs.library, + drawingMode: arch.attrs.drawing_mode, + drawingPath: arch.attrs.drawing_path + }); + this.$el.addClass('o_field_x2many o_field_x2many_' + viewType); + return this.renderer.appendTo(this.$el); + } + return this._super(); + }, + /** + * Override + */ + _renderButtons: function () { + this._super.apply(this, arguments); + if (this.view.arch.tag === 'map') { + var options = {create_text: this.nodeOptions.create_text, widget: this}; + this.$buttons = $(qweb.render('MapView.buttons', options)); + this.$buttons.on('click', 'button.o-map-button-new', this._onAddRecord.bind(this)); + this.$buttons.on('click', 'button.o-map-button-center-map', this._onMapCenter.bind(this)); + } + }, + _onMapCenter: function (event) { + event.stopPropagation(); + if (this.renderer.mapLibrary === 'geometry') { + this.renderer.mapGeometryCentered(); + } else if (this.renderer.mapLibrary === 'drawing') { + this.renderer.mapShapesCentered(); + } + } + }); + +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/view/map/map_controller.js b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_controller.js new file mode 100644 index 00000000..2cbf5493 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_controller.js @@ -0,0 +1,181 @@ +odoo.define('web_google_maps.MapController', function (require) { + 'use strict'; + + var Context = require('web.Context'); + var core = require('web.core'); + var BasicController = require('web.BasicController'); + var Domain = require('web.Domain'); + + var _t = core._t; + var qweb = core.qweb; + + var MapController = BasicController.extend({ + custom_events: _.extend({}, BasicController.prototype.custom_events, { + button_clicked: '_onButtonClicked', + kanban_record_delete: '_onRecordDelete', + kanban_record_update: '_onUpdateRecord', + kanban_column_archive_records: '_onArchiveRecords', + }), + /** + * @override + * @param {Object} params + */ + init: function (parent, model, renderer, params) { + this._super.apply(this, arguments); + + this.on_create = params.on_create; + this.hasButtons = params.hasButtons; + }, + /** + * @private + * @param {OdooEvent} event + */ + _onButtonClicked: function (event) { + event.stopPropagation(); + var self = this; + var attrs = event.data.attrs; + var record = event.data.record; + if (attrs.context) { + attrs.context = new Context(attrs.context) + .set_eval_context({ + active_id: record.res_id, + active_ids: [record.res_id], + active_model: record.model, + }); + } + this.trigger_up('execute_action', { + action_data: attrs, + env: { + context: record.getContext(), + currentID: record.res_id, + model: record.model, + resIDs: record.res_ids, + }, + on_closed: function () { + var recordModel = self.model.localData[record.id]; + var group = self.model.localData[recordModel.parentID]; + var parent = self.model.localData[group.parentID]; + + self.model.reload(record.id).then(function (db_id) { + var data = self.model.get(db_id); + var kanban_record = event.target; + kanban_record.update(data); + + // Check if we still need to display the record. Some fields of the domain are + // not guaranteed to be in data. This is for example the case if the action + // contains a domain on a field which is not in the Kanban view. Therefore, + // we need to handle multiple cases based on 3 variables: + // domInData: all domain fields are in the data + // activeInDomain: 'active' is already in the domain + // activeInData: 'active' is available in the data + + var domain = (parent ? parent.domain : group.domain) || []; + var domInData = _.every(domain, function (d) { + return d[0] in data.data; + }); + var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1; + var activeInData = 'active' in data.data; + + // Case # | domInData | activeInDomain | activeInData + // 1 | true | true | true => no domain change + // 2 | true | true | false => not possible + // 3 | true | false | true => add active in domain + // 4 | true | false | false => no domain change + // 5 | false | true | true => no evaluation + // 6 | false | true | false => no evaluation + // 7 | false | false | true => replace domain + // 8 | false | false | false => no evaluation + + // There are 3 cases which cannot be evaluated since we don't have all the + // necessary information. The complete solution would be to perform a RPC in + // these cases, but this is out of scope. A simpler one is to do a try / catch. + + if (domInData && !activeInDomain && activeInData) { + domain = domain.concat([ + ['active', '=', true] + ]); + } else if (!domInData && !activeInDomain && activeInData) { + domain = [ + ['active', '=', true] + ]; + } + try { + var visible = new Domain(domain).compute(data.evalContext); + } catch (e) { + return; + } + if (!visible) { + kanban_record.destroy(); + } + }); + }, + }); + }, + /** + * @private + * @param {OdooEvent} event + */ + _onRecordDelete: function (event) { + this._deleteRecords([event.data.id]); + }, + /** + * @todo should simply use field_changed event... + * + * @private + * @param {OdooEvent} ev + */ + _onUpdateRecord: function (ev) { + var changes = _.clone(ev.data); + ev.data.force_save = true; + this._applyChanges(ev.target.db_id, changes, ev); + }, + /** + * The interface allows in some case the user to archive a column. This is + * what this handler is for. + * + * @private + * @param {OdooEvent} event + */ + _onArchiveRecords: function (event) { + var self = this; + var active_value = !event.data.archive; + var column = event.target; + var record_ids = _.pluck(column.records, 'db_id'); + if (record_ids.length) { + this.model + .toggleActive(record_ids, active_value, column.db_id) + .then(function (db_id) { + var data = self.model.get(db_id); + self._updateEnv(); + }); + } + }, + renderButtons: function ($node) { + if (this.hasButtons) { + this.$buttons = $(qweb.render('MapView.buttons', { + widget: this + })); + this.$buttons.on('click', 'button.o-map-button-new', this._onButtonNew.bind(this)); + this.$buttons.on('click', 'button.o-map-button-center-map', this._onButtonMapCenter.bind(this)); + this.$buttons.appendTo($node); + } + }, + _onButtonMapCenter: function (event) { + event.preventDefault(); + if (this.renderer.mapLibrary === 'geometry') { + this.renderer.mapGeometryCentered(); + } else if (this.renderer.mapLibrary === 'drawing') { + this.renderer.mapShapesCentered(); + } + }, + _onButtonNew: function (event) { + event.preventDefault(); + this.trigger_up('switch_view', { + view_type: 'form', + res_id: undefined + }); + }, + }); + + return MapController; +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/view/map/map_model.js b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_model.js new file mode 100644 index 00000000..90b67a85 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_model.js @@ -0,0 +1,41 @@ +odoo.define('web_google_maps.MapModel', function(require) { + 'use strict'; + + var BasicModel = require('web.BasicModel'); + + var MapModel = BasicModel.extend({ + /** + * @override + */ + reload: function (id, options) { + if (options && options.groupBy && !options.groupBy.length) { + options.groupBy = this.defaultGroupedBy; + } + return this._super.apply(this, arguments); + }, + /** + * @override + */ + load: function (params) { + this.defaultGroupedBy = params.groupBy; + params.groupedBy = (params.groupedBy && params.groupedBy.length) ? params.groupedBy : this.defaultGroupedBy; + return this._super(params); + }, + /** + * Ensures that there is no nested groups in Map (only the first grouping + * level is taken into account). + * + * @override + */ + _readGroup: function (list) { + var self = this; + if (list.groupedBy.length > 1) { + list.groupedBy = [list.groupedBy[0]]; + } + return this._super.apply(this, arguments); + } + }); + + return MapModel; + +}); diff --git a/ext/custom-addons/web_google_maps/static/src/js/view/map/map_renderer.js b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_renderer.js new file mode 100644 index 00000000..6cc3407b --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_renderer.js @@ -0,0 +1,1410 @@ +odoo.define('web_google_maps.MapRenderer', function (require) { + 'use strict'; + + var BasicRenderer = require('web.BasicRenderer'); + var core = require('web.core'); + var QWeb = require('web.QWeb'); + var session = require('web.session'); + var utils = require('web.utils'); + var Widget = require('web.Widget'); + var KanbanRecord = require('web.KanbanRecord'); + + var qweb = core.qweb; + + var MapRecord = KanbanRecord.extend({ + init: function (parent, state, options) { + this._super.apply(this, arguments); + this.fieldsInfo = state.fieldsInfo.map; + } + }); + + var MAP_THEMES = { + 'default': [], + 'aubergine': [{ + "elementType": "geometry", + "stylers": [{ + "color": "#1d2c4d" + }] + }, + { + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#8ec3b9" + }] + }, + { + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#1a3646" + }] + }, + { + "featureType": "administrative.country", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#4b6878" + }] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#64779e" + }] + }, + { + "featureType": "administrative.province", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#4b6878" + }] + }, + { + "featureType": "landscape.man_made", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#334e87" + }] + }, + { + "featureType": "landscape.natural", + "elementType": "geometry", + "stylers": [{ + "color": "#023e58" + }] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [{ + "color": "#283d6a" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#6f9ba5" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#1d2c4d" + }] + }, + { + "featureType": "poi.business", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "poi.park", + "elementType": "geometry.fill", + "stylers": [{ + "color": "#023e58" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#3C7680" + }] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [{ + "color": "#304a7d" + }] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#98a5be" + }] + }, + { + "featureType": "road", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#1d2c4d" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [{ + "color": "#2c6675" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#255763" + }] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#b0d5ce" + }] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#023e58" + }] + }, + { + "featureType": "transit", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#98a5be" + }] + }, + { + "featureType": "transit", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#1d2c4d" + }] + }, + { + "featureType": "transit.line", + "elementType": "geometry.fill", + "stylers": [{ + "color": "#283d6a" + }] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [{ + "color": "#3a4762" + }] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [{ + "color": "#0e1626" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#4e6d70" + }] + } + ], + 'night': [{ + "elementType": "geometry", + "stylers": [{ + "color": "#242f3e" + }] + }, + { + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#746855" + }] + }, + { + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#242f3e" + }] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#d59563" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#d59563" + }] + }, + { + "featureType": "poi.business", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [{ + "color": "#263c3f" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#6b9a76" + }] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [{ + "color": "#38414e" + }] + }, + { + "featureType": "road", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#212a37" + }] + }, + { + "featureType": "road", + "elementType": "labels.icon", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#9ca5b3" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [{ + "color": "#746855" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#1f2835" + }] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#f3d19c" + }] + }, + { + "featureType": "transit", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "transit", + "elementType": "geometry", + "stylers": [{ + "color": "#2f3948" + }] + }, + { + "featureType": "transit.station", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#d59563" + }] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [{ + "color": "#17263c" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#515c6d" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#17263c" + }] + } + ], + 'dark': [{ + "elementType": "geometry", + "stylers": [{ + "color": "#212121" + }] + }, + { + "elementType": "labels.icon", + "stylers": [{ + "visibility": "off" + }] + }, + { + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#757575" + }] + }, + { + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#212121" + }] + }, + { + "featureType": "administrative", + "elementType": "geometry", + "stylers": [{ + "color": "#757575" + }] + }, + { + "featureType": "administrative.country", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#9e9e9e" + }] + }, + { + "featureType": "administrative.land_parcel", + "stylers": [{ + "visibility": "off" + }] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#bdbdbd" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#757575" + }] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [{ + "color": "#181818" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#616161" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#1b1b1b" + }] + }, + { + "featureType": "road", + "elementType": "geometry.fill", + "stylers": [{ + "color": "#2c2c2c" + }] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#8a8a8a" + }] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [{ + "color": "#373737" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [{ + "color": "#3c3c3c" + }] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry", + "stylers": [{ + "color": "#4e4e4e" + }] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#616161" + }] + }, + { + "featureType": "transit", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#757575" + }] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [{ + "color": "#000000" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#3d3d3d" + }] + } + ], + 'retro': [{ + "elementType": "geometry", + "stylers": [{ + "color": "#ebe3cd" + }] + }, + { + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#523735" + }] + }, + { + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#f5f1e6" + }] + }, + { + "featureType": "administrative", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#c9b2a6" + }] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#dcd2be" + }] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#ae9e90" + }] + }, + { + "featureType": "landscape.natural", + "elementType": "geometry", + "stylers": [{ + "color": "#dfd2ae" + }] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [{ + "color": "#dfd2ae" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#93817c" + }] + }, + { + "featureType": "poi.park", + "elementType": "geometry.fill", + "stylers": [{ + "color": "#a5b076" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#447530" + }] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [{ + "color": "#f5f1e6" + }] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [{ + "color": "#fdfcf8" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [{ + "color": "#f8c967" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#e9bc62" + }] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry", + "stylers": [{ + "color": "#e98d58" + }] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry.stroke", + "stylers": [{ + "color": "#db8555" + }] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#806b63" + }] + }, + { + "featureType": "transit.line", + "elementType": "geometry", + "stylers": [{ + "color": "#dfd2ae" + }] + }, + { + "featureType": "transit.line", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#8f7d77" + }] + }, + { + "featureType": "transit.line", + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#ebe3cd" + }] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [{ + "color": "#dfd2ae" + }] + }, + { + "featureType": "water", + "elementType": "geometry.fill", + "stylers": [{ + "color": "#b9d3c2" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#92998d" + }] + } + ], + 'silver': [{ + "elementType": "geometry", + "stylers": [{ + "color": "#f5f5f5" + }] + }, + { + "elementType": "labels.icon", + "stylers": [{ + "visibility": "off" + }] + }, + { + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#616161" + }] + }, + { + "elementType": "labels.text.stroke", + "stylers": [{ + "color": "#f5f5f5" + }] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#bdbdbd" + }] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [{ + "color": "#eeeeee" + }] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#757575" + }] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [{ + "color": "#e5e5e5" + }] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#9e9e9e" + }] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [{ + "color": "#ffffff" + }] + }, + { + "featureType": "road.arterial", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#757575" + }] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [{ + "color": "#dadada" + }] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#616161" + }] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#9e9e9e" + }] + }, + { + "featureType": "transit.line", + "elementType": "geometry", + "stylers": [{ + "color": "#e5e5e5" + }] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [{ + "color": "#eeeeee" + }] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [{ + "color": "#c9c9c9" + }] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#9e9e9e" + }] + } + ] + } + + var markerColors = [ + 'green', 'yellow', 'blue', 'light-green', + 'red', 'magenta', 'black', 'purple', 'orange', + 'pink', 'grey', 'brown', 'cyan', 'white' + ]; + + function findInNode(node, predicate) { + if (predicate(node)) { + return node; + } + if (!node.children) { + return undefined; + } + for (var i = 0; i < node.children.length; i++) { + if (findInNode(node.children[i], predicate)) { + return node.children[i]; + } + } + } + + function qwebAddIf(node, condition) { + if (node.attrs[qweb.prefix + '-if']) { + condition = _.str.sprintf("(%s) and (%s)", node.attrs[qweb.prefix + '-if'], condition); + } + node.attrs[qweb.prefix + '-if'] = condition; + } + + function transformQwebTemplate(node, fields) { + // Process modifiers + if (node.tag && node.attrs.modifiers) { + var modifiers = node.attrs.modifiers || {}; + if (modifiers.invisible) { + qwebAddIf(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible))); + } + } + switch (node.tag) { + case 'button': + case 'a': + var type = node.attrs.type || ''; + if (_.indexOf('action,object,edit,open,delete,url,set_cover'.split(','), type) !== -1) { + _.each(node.attrs, function (v, k) { + if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) !== -1) { + node.attrs['data-' + k] = v; + delete(node.attrs[k]); + } + }); + if (node.attrs['data-string']) { + node.attrs.title = node.attrs['data-string']; + } + if (node.tag === 'a' && node.attrs['data-type'] !== "url") { + node.attrs.href = '#'; + } else { + node.attrs.type = 'button'; + } + + var action_classes = " oe_kanban_action oe_kanban_action_" + node.tag; + if (node.attrs['t-attf-class']) { + node.attrs['t-attf-class'] += action_classes; + } else if (node.attrs['t-att-class']) { + node.attrs['t-att-class'] += " + '" + action_classes + "'"; + } else { + node.attrs['class'] = (node.attrs['class'] || '') + action_classes; + } + } + break; + } + if (node.children) { + for (var i = 0, ii = node.children.length; i < ii; i++) { + transformQwebTemplate(node.children[i], fields); + } + } + } + + var SidebarGroup = Widget.extend({ + template: 'MapView.MapViewGroupInfo', + init: function (parent, options) { + this._super.apply(this, arguments); + this.groups = options.groups; + } + }); + + var MapRenderer = BasicRenderer.extend({ + className: 'o_map_view', + template: 'MapView.MapView', + /** + * @override + */ + init: function (parent, state, params) { + this._super.apply(this, arguments); + this.mapLibrary = params.mapLibrary; + this.widgets = []; + this.mapThemes = MAP_THEMES; + + this.qweb = new QWeb(session.debug, { + _s: session.origin + }, false); + var templates = findInNode(this.arch, function (n) { + return n.tag === 'templates'; + }); + transformQwebTemplate(templates, state.fields); + this.qweb.add_template(utils.json_node_to_xml(templates)); + this.recordOptions = _.extend({}, params.record_options, { + qweb: this.qweb, + viewType: 'map', + }); + this.state = state; + this.shapesCache = {}; + this._initLibraryProperties(params); + }, + _initLibraryProperties: function (params) { + if (this.mapLibrary === 'drawing') { + this.drawingMode = params.drawingMode || 'shape_type'; + this.drawingPath = params.drawingPath || 'shape_paths'; + this.shapesLatLng = []; + } else if (this.mapLibrary === 'geometry') { + this.defaultMarkerColor = 'red'; + this.markerGroupedInfo = []; + this.iconColors = markerColors; + this.markers = []; + this.iconUrl = '/web_google_maps/static/src/img/markers/'; + this.fieldLat = params.fieldLat; + this.fieldLng = params.fieldLng; + this.markerColor = params.markerColor; + this.markerColors = params.markerColors; + this.groupedMarkerColors = _.extend([], params.iconColors); + } + }, + /** + * @override + */ + updateState: function (state) { + this.state = state; + return this._super.apply(this, arguments); + }, + /** + * @override + */ + start: function () { + this._initMap(); + return this._super(); + }, + /** + * Style the map + * @private + */ + _getMapTheme: function () { + var self = this; + var update_map = function (style) { + var styledMapType = new google.maps.StyledMapType(self.mapThemes[style], { + name: style, + }); + self.gmap.setOptions({ + mapTypeControlOptions: { + mapTypeIds: ['roadmap', 'satellite', 'hybrid', 'terrain', 'styled_map'], + style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR, + position: google.maps.ControlPosition.TOP_CENTER + } + }); + //Associate the styled map with the MapTypeId and set it to display. + self.gmap.mapTypes.set('styled_map', styledMapType); + self.gmap.setMapTypeId('styled_map'); + } + if (!this.theme) { + this._rpc({ + route: '/web/map_theme' + }).then(function (data) { + if (data.theme && self.mapThemes.hasOwnProperty(data.theme)) { + self.theme = data.theme; + update_map(data.theme); + } + }); + } + }, + /** + * Initialize map + * @private + */ + _initMap: function () { + this.infoWindow = new google.maps.InfoWindow(); + this.$('.o_map_view').empty(); + this.gmap = new google.maps.Map(this.$('.o_map_view').get(0), { + mapTypeId: google.maps.MapTypeId.ROADMAP, + minZoom: 3, + maxZoom: 20, + fullscreenControl: true, + mapTypeControl: true + }); + this._getMapTheme(); + if (this.mapLibrary === 'geometry') { + this._initMarkerCluster(); + } + this.$right_sidebar = this.$('.o_map_right_sidebar'); + }, + _initMarkerCluster: function () { + this.markerCluster = new MarkerClusterer(this.gmap, [], { + imagePath: '/web_google_maps/static/lib/markercluster/img/m', + gridSize: 20, + maxZoom: 17 + }); + }, + /** + * Compute marker color + * @param {any} record + * @return string + */ + _getIconColor: function (record) { + if (this.markerColor) { + return this.markerColor; + } + + if (!this.markerColors) { + return this.defaultMarkerColor; + } + + var context = _.mapObject(_.extend({}, record.data, { + uid: session.uid, + current_date: moment().format('YYYY-MM-DD') // TODO: time, datetime, relativedelta + }), function (val, key) { + return (val instanceof Array) ? (_.last(val) || '') : val; + }); + for (var i = 0; i < this.markerColors.length; i++) { + var pair = this.markerColors[i]; + var color = pair[0]; + var expression = pair[1]; + if (py.PY_isTrue(py.evaluate(expression, context))) { + return color; + } + // TODO: handle evaluation errors + } + return ''; + }, + /** + * Create marker + * @param {any} latLng: instance of google LatLng + * @param {any} record + * @param {string} color + */ + _createMarker: function (latLng, record, color) { + var options = { + position: latLng, + map: this.gmap, + animation: google.maps.Animation.DROP, + _odooRecord: record + }; + if (color) { + options.icon = this.iconUrl + color.trim() + '.png'; + } + var marker = new google.maps.Marker(options); + this.markers.push(marker); + this._clusterAddMarker(marker); + }, + /** + * Handle Multiple Markers present at the same coordinates + */ + _clusterAddMarker: function (marker) { + var _position; + var markerInClusters = this.markerCluster.getMarkers(); + var existingRecords = []; + if (markerInClusters.length > 0) { + markerInClusters.forEach(function (_cMarker) { + _position = _cMarker.getPosition(); + if (marker.getPosition().equals(_position)) { + existingRecords.push(_cMarker._odooRecord); + } + }); + } + this.markerCluster.addMarker(marker); + google.maps.event.addListener(marker, 'click', this._markerInfoWindow.bind(this, marker, existingRecords)); + }, + /** + * Marker info window + * @param {any} marker: instance of google marker + * @param {any} record + * @return function + */ + _markerInfoWindow: function (marker, currentRecords) { + var self = this; + var _content = ''; + var markerRecords = []; + + var markerDiv = document.createElement('div'); + markerDiv.className = 'o_kanban_view o_kanban_grouped'; + + var markerContent = document.createElement('div'); + markerContent.className = 'o_kanban_group'; + + if (currentRecords.length > 0) { + currentRecords.forEach(function (_record) { + _content = self._generateMarkerInfoWindow(_record); + markerRecords.push(_content); + _content.appendTo(markerContent); + }); + } + + var markerIwContent = this._generateMarkerInfoWindow(marker._odooRecord); + markerIwContent.appendTo(markerContent); + + markerDiv.appendChild(markerContent); + this.infoWindow.setContent(markerDiv); + this.infoWindow.open(this.gmap, marker); + }, + _shapeInfoWindow: function (record, event) { + var markerDiv = document.createElement('div'); + markerDiv.className = 'o_kanban_view o_kanban_grouped'; + + var markerContent = document.createElement('div'); + markerContent.className = 'o_kanban_group'; + + var markerIwContent = this._generateMarkerInfoWindow(record); + markerIwContent.appendTo(markerContent); + + markerDiv.appendChild(markerContent); + this.infoWindow.setContent(markerDiv); + this.infoWindow.setPosition(event.latLng); + this.infoWindow.open(this.gmap); + }, + /** + * @private + */ + _generateMarkerInfoWindow: function (record) { + var markerIw = new MapRecord(this, record, this.recordOptions); + return markerIw; + }, + /** + * Render markers + * @private + * @param {Object} record + */ + _renderMarkers: function () { + var isGrouped = !!this.state.groupedBy.length; + if (isGrouped) { + this._renderGrouped(); + } else { + this._renderUngrouped(); + } + }, + _renderGrouped: function () { + var self = this; + var color; + var latLng; + + _.each(this.state.data, function (record) { + color = self._getGroupedMarkerColor(); + record.markerColor = color; + _.each(record.data, function (rec) { + latLng = new google.maps.LatLng(rec.data[self.fieldLat], rec.data[self.fieldLng]); + self._createMarker(latLng, rec, color); + }); + self.markerGroupedInfo.push({ + 'title': record.value || 'Undefined', + 'count': record.count, + 'marker': self.iconUrl + record.markerColor.trim() + '.png' + }); + }); + }, + _renderUngrouped: function () { + var self = this; + var color; + var latLng; + + _.each(this.state.data, function (record) { + color = self._getIconColor(record); + latLng = new google.maps.LatLng(record.data[self.fieldLat], record.data[self.fieldLng]); + record.markerColor = color; + self._createMarker(latLng, record, color); + }); + }, + /** + * Get color + * @returns {string} + */ + _getGroupedMarkerColor: function () { + var color; + if (this.groupedMarkerColors.length) { + color = this.groupedMarkerColors.splice(0, 1)[0]; + } else { + this.groupedMarkerColors = _.extend([], this.iconColors); + color = this.groupedMarkerColors.splice(0, 1)[0]; + } + return color; + }, + _drawPolygon: function (record) { + var polygon; + var path = record.data[this.drawingPath]; + var value = JSON.parse(path); + if (Object.keys(value).length > 0) { + if (this.shapesCache[record.data.id] === undefined) { + polygon = new google.maps.Polygon({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + map: this.gmap + }); + polygon.setOptions(value.options); + this.shapesCache[record.data.id] = polygon; + } else { + polygon = this.shapesCache[record.data.id]; + polygon.setMap(this.gmap); + } + this.shapesLatLng = this.shapesLatLng.concat(value.options.paths); + polygon.addListener('click', this._shapeInfoWindow.bind(this, record)); + } + }, + _drawCircle: function (record) { + var circle; + var path = record.data[this.drawingPath]; + var value = JSON.parse(path); + if (Object.keys(value).length > 0) { + if (this.shapesCache[record.data.id] === undefined) { + circle = new google.maps.Circle({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + map: this.gmap + }); + circle.setOptions(value.options); + this.shapesCache[record.data.id] = circle; + } else { + circle = this.shapesCache[record.data.id]; + circle.setMap(this.gmap); + } + this.shapesBounds.union(circle.getBounds()); + circle.addListener('click', this._shapeInfoWindow.bind(this, record)); + } + }, + /** + * Draw rectangle + * @param {Object} record + */ + _drawRectangle: function (record) { + var rectangle; + var path = record.data[this.drawingPath]; + var value = JSON.parse(path); + if (Object.keys(value).length > 0) { + var shapeOptions = value.options; + if (this.shapesCache[record.data.id] === undefined) { + rectangle = new google.maps.Rectangle({ + strokeColor: '#FF0000', + strokeOpacity: 0.85, + strokeWeight: 1.0, + fillColor: '#FF9999', + fillOpacity: 0.35, + map: this.gmap + }); + rectangle.setOptions(shapeOptions); + this.shapesCache[record.data.id] = rectangle; + } else { + rectangle = this.shapesCache[record.data.id]; + rectangle.setMap(this.gmap); + } + + this.shapesBounds.union(rectangle.getBounds()); + rectangle.addListener('click', this._shapeInfoWindow.bind(this, record)); + } + }, + /** + * Draw shape into the map + */ + _renderShapes: function () { + var self = this; + var shapesToKeep = []; + this.shapesBounds = new google.maps.LatLngBounds(); + _.each(this.state.data, function (record) { + if (record.data.hasOwnProperty('id')) { + shapesToKeep.push((record.data.id).toString()); + } + if (record.data[self.drawingMode] === 'polygon') { + self._drawPolygon(record); + } else if (record.data[self.drawingMode] === 'rectangle') { + self._drawRectangle(record); + } else if (record.data[self.drawingMode] === 'circle') { + self._drawCircle(record); + } + }); + this._cleanShapesInCache(shapesToKeep); + }, + /** + * @private + * @param {Array} ShapesToKeep contains list of id + * Remove shapes from the maps without deleting the shape + * will keep those shapes in cache + */ + _cleanShapesInCache: function (shapesToKeep) { + _.each(this.shapesCache, function (shape, id) { + if (shapesToKeep.indexOf(id) === -1) { + shape.setMap(null); + } + }); + }, + /** + * @override + */ + _renderView: function () { + var self = this; + if (this.mapLibrary === 'geometry') { + this.markerGroupedInfo.length = 0; + this._clearMarkerClusters(); + this._renderMarkers(); + return this._super.apply(this, arguments) + .then(self._renderSidebarGroup.bind(self)) + .then(self._clusterMarkers.bind(self)) + .then(self.mapGeometryCentered.bind(self)); + } else if (this.mapLibrary === 'drawing') { + this.shapesLatLng.length = 0; + this._renderShapes(); + return this._super.apply(this, arguments).then(this.mapShapesCentered.bind(this)); + } + return this._super.apply(this, arguments); + }, + /** + * Cluster markers + * @private + */ + _clusterMarkers: function () { + this.markerCluster.addMarkers(this.markers); + }, + /** + * Centering map + */ + mapShapesCentered: function () { + var mapBounds = new google.maps.LatLngBounds(); + if (!this.shapesBounds.isEmpty()) { + mapBounds.union(this.shapesBounds); + } + _.each(this.shapesLatLng, function (latLng) { + mapBounds.extend(latLng); + }); + this.gmap.fitBounds(mapBounds); + }, + /** + * Centering map + */ + mapGeometryCentered: function () { + var self = this; + var mapBounds = new google.maps.LatLngBounds(); + + _.each(this.markers, function (marker) { + mapBounds.extend(marker.getPosition()); + }); + this.gmap.fitBounds(mapBounds); + + google.maps.event.addListenerOnce(this.gmap, 'idle', function () { + google.maps.event.trigger(self.gmap, 'resize'); + if (self.gmap.getZoom() > 17) self.gmap.setZoom(17); + }); + }, + /** + * Clear marker clusterer and list markers + * @private + */ + _clearMarkerClusters: function () { + this.markerCluster.clearMarkers(); + this.markers = []; + }, + /** + * Render a sidebar for grouped markers info + * @private + */ + _renderSidebarGroup: function () { + var self = this; + if (this.markerGroupedInfo.length > 0) { + this.$right_sidebar.empty().removeClass('closed').addClass('open'); + var groupInfo = new SidebarGroup(this, { + 'groups': this.markerGroupedInfo + }); + groupInfo.appendTo(this.$right_sidebar); + } else { + this.$right_sidebar.empty(); + if (!this.$right_sidebar.hasClass('closed')) { + this.$right_sidebar.removeClass('open').addClass('closed'); + } + } + }, + /** + * Sets the current state and updates some internal attributes accordingly. + * + * @private + * @param {Object} state + */ + _setState: function (state) { + this.state = state; + + var groupByFieldAttrs = state.fields[state.groupedBy[0]]; + var groupByFieldInfo = state.fieldsInfo.map[state.groupedBy[0]]; + // Deactivate the drag'n'drop if the groupedBy field: + // - is a date or datetime since we group by month or + // - is readonly (on the field attrs or in the view) + var draggable = false; + if (groupByFieldAttrs) { + if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") { + draggable = false; + } else if (groupByFieldAttrs.readonly !== undefined) { + draggable = !(groupByFieldAttrs.readonly); + } + } + if (groupByFieldInfo) { + if (draggable && groupByFieldInfo.readonly !== undefined) { + draggable = !(groupByFieldInfo.readonly); + } + } + this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one'); + }, + }); + + return MapRenderer; + +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/view/map/map_view.js b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_view.js new file mode 100644 index 00000000..a040d696 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/view/map/map_view.js @@ -0,0 +1,81 @@ +odoo.define('web_google_maps.MapView', function (require) { + 'use strict'; + + var BasicView = require('web.BasicView'); + var core = require('web.core'); + + var MapModel = require('web_google_maps.MapModel'); + var MapRenderer = require('web_google_maps.MapRenderer'); + var MapController = require('web_google_maps.MapController'); + + var _lt = core._lt; + + var MapView = BasicView.extend({ + accesskey: 'm', + display_name: _lt('Map'), + icon: 'fa-map-o', + jsLibs: [], + config: _.extend({}, BasicView.prototype.config, { + Model: MapModel, + Renderer: MapRenderer, + Controller: MapController + }), + viewType: 'map', + init: function (viewInfo, params) { + this._super.apply(this, arguments); + + var arch = viewInfo.arch; + var attrs = arch.attrs; + + var activeActions = this.controllerParams.activeActions; + var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly"; + + this.loadParams.limit = this.loadParams.limit || 40; + this.loadParams.openGroupByDefault = true; + this.loadParams.type = 'list'; + + this.loadParams.groupBy = arch.attrs.default_group_by ? [arch.attrs.default_group_by] : (params.groupBy || []); + + this.rendererParams.arch = arch; + this.rendererParams.mapLibrary = attrs.library; + + if (attrs.library === 'drawing') { + this.rendererParams.drawingMode = attrs.drawing_mode; + this.rendererParams.drawingPath = attrs.drawing_path; + } else if (attrs.library === 'geometry') { + var colors = this._setMarkersColor(attrs.colors); + this.rendererParams.markerColor = attrs.color; + this.rendererParams.markerColors = colors; + this.rendererParams.fieldLat = attrs.lat; + this.rendererParams.fieldLng = attrs.lng; + } + + this.rendererParams.record_options = { + editable: activeActions.edit, + deletable: activeActions.delete, + read_only_mode: params.readOnlyMode || true, + }; + + this.controllerParams.mode = mode; + this.controllerParams.hasButtons = true; + }, + _setMarkersColor: function (colors) { + var pair, color, expr; + if (!colors) { + return false; + } + return _(colors.split(';')) + .chain() + .compact() + .map(function (color_pair) { + pair = color_pair.split(':'); + color = pair[0]; + expr = pair[1]; + return [color, py.parse(py.tokenize(expr)), expr]; + }).value(); + } + }); + + return MapView; + +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/view/view_registry.js b/ext/custom-addons/web_google_maps/static/src/js/view/view_registry.js new file mode 100644 index 00000000..18c24a1b --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/view/view_registry.js @@ -0,0 +1,9 @@ +odoo.define('web_google_maps.view_registry', function (require) { + "use strict"; + + var MapView = require('web_google_maps.MapView'); + var view_registry = require('web.view_registry'); + + view_registry.add('map', MapView); + +}); diff --git a/ext/custom-addons/web_google_maps/static/src/js/widgets/fields_registry.js b/ext/custom-addons/web_google_maps/static/src/js/widgets/fields_registry.js new file mode 100644 index 00000000..bee34b6f --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/widgets/fields_registry.js @@ -0,0 +1,10 @@ +odoo.define('web_google_maps.FieldsRegistry', function(require) { + 'use strict'; + + var registry = require('web.field_registry'); + var GplacesAutocomplete = require('web_google_maps.GplaceAutocompleteFields'); + + registry.add('gplaces_address_autocomplete', GplacesAutocomplete.GplacesAddressAutocompleteField); + registry.add('gplaces_autocomplete', GplacesAutocomplete.GplacesAutocompleteField); + +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/widgets/gplaces_autocomplete.js b/ext/custom-addons/web_google_maps/static/src/js/widgets/gplaces_autocomplete.js new file mode 100644 index 00000000..e5e50b20 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/widgets/gplaces_autocomplete.js @@ -0,0 +1,496 @@ +odoo.define('web_google_maps.GplaceAutocompleteFields', function (require) { + 'use strict'; + + var BasicFields = require('web.basic_fields'); + var core = require('web.core'); + var Utils = require('web_google_maps.Utils'); + var _t = core._t; + + var GplaceAutocomplete = BasicFields.InputField.extend({ + className: 'o_field_char o_field_google_autocomplete', + tagName: 'span', + supportedFieldTypes: ['char'], + events: _.extend({}, BasicFields.InputField.prototype.events, { + 'focus': '_geolocate' + }), + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this._type_relations = ['one2many', 'many2one', 'many2many']; + this.places_autocomplete = false; + this.component_form = Utils.GOOGLE_PLACES_COMPONENT_FORM; + this.address_form = Utils.ADDRESS_FORM; + this.fillfields_delimiter = { + street: " ", + street2: ", ", + }; + this.fillfields = {}; + this.lng = false; + this.lat = false; + this.setDefault(); + }, + /** + * @override + */ + start: function () { + return this._super.apply(this, arguments).then(this.prepareWidgetOptions.bind(this)); + }, + /** + * Set widget default value + */ + setDefault: function () {}, + /** + * Prepare widget options + */ + prepareWidgetOptions: function () { + if (this.mode === 'edit') { + // update 'component_form', 'delimiter' if exists + if (this.attrs.options) { + if (this.attrs.options.hasOwnProperty('component_form')) { + this.component_form = _.defaults({}, this.attrs.options.component_form, this.component_form); + } + if (this.attrs.options.hasOwnProperty('delimiter')) { + this.fillfields_delimiter = _.defaults({}, this.attrs.options.delimiter, this.fillfields_delimiter); + } + if (this.attrs.options.hasOwnProperty('lat')) { + this.lat = this.attrs.options.lat; + } + if (this.attrs.options.hasOwnProperty('lng')) { + this.lng = this.attrs.options.lng; + } + if (this.attrs.options.hasOwnProperty('address_form')) { + this.address_form = _.defaults({}, this.attrs.options.address_form, this.address_form); + } + } + this.target_fields = this.getFillFieldsType(); + } + }, + /** + * To be overriden + */ + getFillFieldsType: function () { + return []; + }, + /** + * Geolocate + * @private + */ + _geolocate: function () { + var self = this; + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function (position) { + var geolocation = { + lat: position.coords.latitude, + lng: position.coords.longitude + }; + + var circle = new google.maps.Circle({ + center: geolocation, + radius: position.coords.accuracy + }); + + self.places_autocomplete.setBounds(circle.getBounds()); + }); + } + }, + /** + * @private + */ + _prepareValue: function (model, field_name, value) { + var model = model || false; + var field_name = field_name || false; + var value = value || false; + return Utils.fetchValues(model, field_name, value); + }, + /** + * @private + */ + _populateAddress: function (place, fillfields, delimiter) { + var place = place || false; + var fillfields = fillfields || this.fillfields; + var delimiter = delimiter || this.fillfields_delimiter; + return Utils.gmaps_populate_address(place, fillfields, delimiter); + }, + /** + * @private + */ + _populatePlaces: function (place, fillfields) { + var place = place || false; + var fillfields = fillfields || this.fillfields; + return Utils.gmaps_populate_places(place, fillfields); + }, + /** + * @private + */ + _getCountryState: function (model, country, state) { + var model = model || false; + var country = country || false; + var state = state || false; + return Utils.fetchCountryState(model, country, state); + }, + /** + * @private + */ + _setGeolocation: function (latitude, longitude) { + var res = {}; + if (_.intersection(_.keys(this.record.fields), [this.lat, this.lng]).length == 2) { + res[this.lat] = latitude; + res[this.lng] = longitude; + } + return res; + }, + /** + * @private + */ + _onUpdateWidgetFields: function (changes) { + var changes = changes || {}; + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: changes, + viewType: this.viewType, + }); + }, + /** + * @override + */ + _renderEdit: function () { + this._super.apply(this, arguments); + if (this.isValid()) { + this._initGplacesAutocomplete(); + } + }, + /** + * @override + */ + destroy: function () { + if (this.places_autocomplete) { + this.places_autocomplete.unbindAll(); + } + // Remove all PAC container in DOM if any + $('.pac-container').remove(); + return this._super(); + } + + }); + + var GplaceAddressAutocompleteField = GplaceAutocomplete.extend({ + setDefault: function () { + this._super.apply(this, arguments); + this.fillfields = { + [this.address_form.street]: ['street_number', 'route'], + [this.address_form.street2]: ['administrative_area_level_3', 'administrative_area_level_4', 'administrative_area_level_5'], + [this.address_form.city]: ['locality', 'administrative_area_level_2'], + [this.address_form.zip]: 'postal_code', + [this.address_form.state_id]: 'administrative_area_level_1', + [this.address_form.country_id]: 'country' + }; + }, + /** + * @override + */ + prepareWidgetOptions: function () { + if (this.mode === 'edit' && this.attrs.options) { + if (this.attrs.options.hasOwnProperty('fillfields')) { + this.fillfields = _.defaults({}, this.attrs.options.fillfields, this.fillfields); + } + } + this._super(); + }, + /** + * Get fields attributes + * @override + */ + getFillFieldsType: function () { + var self = this; + var field; + var res = this._super(); + if (this._isValid) { + _.each(this.fillfields, function (val, name) { + if (_.contains(self._type_relations, self.record.fields[name].type)) { + field = { + name: name, + type: self.record.fields[name].type, + relation: self.record.fields[name].relation + }; + res.push(field); + } else { + field = { + name: name, + type: self.record.fields[name].type, + relation: false + }; + res.push(field); + } + }); + } + return res; + }, + _initGplacesAutocomplete: function () { + var self = this; + if (!this.places_autocomplete) { + this.places_autocomplete = new google.maps.places.Autocomplete(this.$input.get(0), { + types: ['address'] + }); + } + // When the user selects an address from the dropdown, populate the address fields in the form. + this.place_listener = this.places_autocomplete.addListener('place_changed', function () { + var requests = []; + var place = this.getPlace(); + if (place.hasOwnProperty('address_components')) { + var google_address = self._populateAddress(place); + _.each(self.target_fields, function (field) { + requests.push(self._prepareValue(field.relation, field.name, google_address[field.name])); + }); + + var partner_geometry = self._setGeolocation(place.geometry.location.lat(), place.geometry.location.lng()); + _.each(partner_geometry, function (val, field) { + requests.push(self._prepareValue(false, field, val)); + }); + + $.when.apply($, requests).done(function () { + var changes = {}; + _.each(arguments, function (data, idx) { + _.each(data, function (val, key) { + if (typeof val === 'object') { + changes[key] = val; + } else { + changes[key] = self._parseValue(val); + } + }); + }); + if (!changes[self.address_form.state_id] && + changes[self.address_form.country_id].hasOwnProperty('id') && + google_address[self.address_form.state_id]) { + var state_id = _.find(self.target_fields, function(field) { + return field.name === self.address_form.state_id; + }) + if (!state_id) return; + self._getCountryState( + state_id.relation, + changes[self.address_form.country_id].id, + google_address[self.address_form.state_id] + ).then(function (result) { + var state = { + [self.address_form.state_id]: result.length == 1 ? result[0] : false, + } + _.extend(changes, state); + self._onUpdateWidgetFields(changes); + }); + } else { + self._onUpdateWidgetFields(changes); + } + }); + self.$input.val(google_address[self.name]); + } + }); + }, + /** + * @override + */ + isValid: function () { + this._super.apply(this, arguments); + var self = this, + unknown_fields; + + unknown_fields = _.filter(_.keys(self.fillfields), function (field) { + return !self.record.fields.hasOwnProperty(field); + }); + if (unknown_fields.length > 0) { + self.do_warn(_t('The following fields are invalid:'), _t('
  • ' + unknown_fields.join('
  • ') + '
')); + this._isValid = false; + } + return this._isValid; + }, + /** + * @override + */ + destroy: function () { + if (this.places_autocomplete) { + google.maps.event.removeListener(this.place_listener); + google.maps.event.clearInstanceListeners(this.places_autocomplete); + } + return this._super(); + } + }); + + var GplacesAutocompleteField = GplaceAutocomplete.extend({ + setDefault: function () { + this._super.apply(this); + this.fillfields = { + general: { + name: 'name', + website: 'website', + phone: ['international_phone_number', 'formatted_phone_number'] + }, + address: { + [this.address_form.street]: ['street_number', 'route'], + [this.address_form.street2]: ['administrative_area_level_3', 'administrative_area_level_4', 'administrative_area_level_5'], + [this.address_form.city]: ['locality', 'administrative_area_level_2'], + [this.address_form.zip]: 'postal_code', + [this.address_form.state_id]: 'administrative_area_level_1', + [this.address_form.country_id]: 'country' + } + }; + }, + prepareWidgetOptions: function () { + if (this.mode === 'edit' && this.attrs.options) { + if (this.attrs.options.hasOwnProperty('fillfields')) { + if (this.attrs.options.fillfields.hasOwnProperty('address')) { + this.fillfields['address'] = _.defaults({}, this.attrs.options.fillfields.address, this.fillfields.address); + } + if (this.attrs.options.fillfields.hasOwnProperty('general')) { + this.fillfields['general'] = _.defaults({}, this.attrs.options.fillfields.general, this.fillfields.general); + } + if (this.attrs.options.fillfields.hasOwnProperty('geolocation')) { + this.fillfields['geolocation'] = this.attrs.options.fillfields.geolocation; + } + } + } + this._super(); + }, + getFillFieldsType: function () { + var self = this; + var field; + var res = this._super(); + if (this._isValid) { + for (var option in this.fillfields) { + _.each(this.fillfields[option], function (val, name) { + if (_.contains(self._type_relations, self.record.fields[name].type)) { + field = { + name: name, + type: self.record.fields[name].type, + relation: self.record.fields[name].relation + }; + res.push(field); + } else { + field = { + name: name, + type: self.record.fields[name].type, + relation: false + }; + res.push(field); + } + }); + } + } + return res; + }, + _setGeolocation: function (lat, lng) { + var res = {}; + if (this.lat && this.lng) { + var _res = this._super(lat, lng); + return _res; + } else if (this.fillfields.geolocation) { + _.each(this.fillfields.geolocation, function (alias, field) { + if (alias === 'latitude') { + res[field] = lat; + } + if (alias === 'longitude') { + res[field] = lng; + } + }); + } + return res; + }, + _initGplacesAutocomplete: function () { + var self = this; + this.places_autocomplete = new google.maps.places.Autocomplete(this.$input.get(0), { + types: ['establishment'] + }); + // When the user selects an address from the dropdown, populate the address fields in the form. + this.place_listener = this.places_autocomplete.addListener('place_changed', function () { + var values = {}, + requests = [], + place; + place = this.getPlace(); + if (place.hasOwnProperty('address_components')) { + // Get address + var google_address = self._populateAddress(place, self.fillfields.address, self.fillfields_delimiter); + _.extend(values, google_address); + // Get place (name, website, phone) + var google_place = self._populatePlaces(place, self.fillfields.general); + _.extend(values, google_place); + // Get place geolocation + var google_geolocation = self._setGeolocation(place.geometry.location.lat(), place.geometry.location.lng()); + _.extend(values, google_geolocation); + + _.each(self.target_fields, function (field) { + requests.push(self._prepareValue(field.relation, field.name, values[field.name])); + }); + + $.when.apply($, requests).done(function () { + var changes = {} + _.each(arguments, function (data, idx) { + _.each(data, function (val, key) { + if (typeof val === 'object') { + changes[key] = val; + } else { + changes[key] = self._parseValue(val); + } + }); + }); + if (!changes[self.address_form.state_id] && + changes[self.address_form.country_id].hasOwnProperty('id') && + google_address[self.address_form.state_id]) { + var state_id = _.find(self.target_fields, function(field) { + return field.name === self.address_form.state_id; + }) + if (!state_id) return; + self._getCountryState( + state_id.relation, + changes[self.address_form.country_id].id, + google_address[self.address_form.state_id] + ).then(function (result) { + var state = { + [self.address_form.state_id]: result.length == 1 ? result[0] : false, + } + _.extend(changes, state); + self._onUpdateWidgetFields(changes); + }); + } else { + self._onUpdateWidgetFields(changes); + } + }); + self.$input.val(place.name); + } + }); + }, + /** + * @override + */ + isValid: function () { + this._super.apply(this, arguments); + var self = this, + unknown_fields; + for (var option in this.fillfields) { + unknown_fields = _.filter(_.keys(this.fillfields[option]), function (field) { + return !self.record.fields.hasOwnProperty(field); + }); + if (unknown_fields.length > 0) { + self.do_warn(_t('The following fields are invalid:'), _t('
  • ' + unknown_fields.join('
  • ') + '
')); + this._isValid = false; + } + } + return this._isValid; + }, + /** + * @override + */ + destroy: function () { + if (this.places_autocomplete) { + google.maps.event.removeListener(this.place_listener); + google.maps.event.clearInstanceListeners(this.$input.get(0)); + } + return this._super(); + } + }); + + return { + GplacesAddressAutocompleteField: GplaceAddressAutocompleteField, + GplacesAutocompleteField: GplacesAutocompleteField + }; + +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/js/widgets/utils.js b/ext/custom-addons/web_google_maps/static/src/js/widgets/utils.js new file mode 100644 index 00000000..f9a06831 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/js/widgets/utils.js @@ -0,0 +1,164 @@ +odoo.define('web_google_maps.Utils', function (require) { + 'use strict'; + + var rpc = require("web.rpc"); + + var GOOGLE_PLACES_COMPONENT_FORM = { + street_number: 'long_name', + route: 'long_name', + intersection: 'short_name', + political: 'short_name', + country: 'short_name', + administrative_area_level_1: 'short_name', + administrative_area_level_2: 'short_name', + administrative_area_level_3: 'short_name', + administrative_area_level_4: 'short_name', + administrative_area_level_5: 'short_name', + colloquial_area: 'short_name', + locality: 'short_name', + ward: 'short_name', + sublocality_level_1: 'short_name', + sublocality_level_2: 'short_name', + sublocality_level_3: 'short_name', + sublocality_level_5: 'short_name', + neighborhood: 'short_name', + premise: 'short_name', + postal_code: 'short_name', + natural_feature: 'short_name', + airport: 'short_name', + park: 'short_name', + point_of_interest: 'long_name' + }; + /** + * Mapping field with an alias + * key: alias + * value: field + */ + var ADDRESS_FORM = { + street: 'street', + street2: 'street2', + city: 'city', + zip: 'zip', + state_id: 'state_id', + country_id: 'country_id' + }; + + function fetchValues(model, field_name, value) { + var def = $.Deferred(), + res = {}; + + if (model && value) { + rpc.query({ + 'model': model, + 'method': 'search_read', + 'args': [['|', ['name', '=', value], ['code', '=', value]], ['display_name', ]] + }).then(function (record) { + res[field_name] = record.length == 1 ? record[0] : false; + def.resolve(res); + }); + } else { + res[field_name] = value; + def.resolve(res); + } + return def; + } + + function fetchCountryState(model, country, state) { + var def = $.Deferred(); + + if (country && state) { + rpc.query({ + model: model, + method: 'search_read', + args: [[['country_id', '=', country], ['code', '=', state]], ['display_name']] + }).then(function(record) { + def.resolve(record); + }); + } else { + def.resolve([]); + } + return def; + } + + function gmaps_get_geolocation(place, options) { + if (!place) return {}; + + var vals = {}; + _.each(options, function (alias, field) { + if (alias === 'latitude') { + vals[field] = place.geometry.location.lat(); + } else if (alias === 'longitude') { + vals[field] = place.geometry.location.lng(); + } + }); + return vals; + } + + function gmaps_populate_places(place, place_options) { + if (!place) return {}; + + var values = {}, + vals; + _.each(place_options, function (option, field) { + if (option instanceof Array && !_.has(values, field)) { + vals = _.filter(_.map(option, function (opt) { + return place[opt] || false; + })); + values[field] = _.first(vals) || ""; + } else { + values[field] = place[option] || ""; + } + }); + return values; + } + + function gmaps_populate_address(place, address_options, delimiter) { + if (!place) return {}; + + var address_options = address_options || {}, + fields_delimiter = delimiter || { + street: " ", + street2: ", " + }, + fields_to_fill = {}, + options, temp, dlmter, result = {}; + + // initialize object key and value + _.each(address_options, function (value, key) { + fields_to_fill[key] = []; + }); + + _.each(address_options, function (options, field) { + // turn all fields options into an Array + options = _.flatten([options]); + temp = {}; + _.each(place.address_components, function (component) { + _.each(_.intersection(options, component.types), function (match) { + temp[match] = component[GOOGLE_PLACES_COMPONENT_FORM[match]] || false; + }); + }); + fields_to_fill[field] = _.map(options, function (item) { + return temp[item]; + }); + }); + + _.each(fields_to_fill, function (value, key) { + dlmter = fields_delimiter[key] || ' '; + if (key == 'city') { + result[key] = _.first(_.filter(value)) || ''; + } else { + result[key] = _.filter(value).join(dlmter); + } + }); + return result; + } + return { + 'GOOGLE_PLACES_COMPONENT_FORM': GOOGLE_PLACES_COMPONENT_FORM, + 'ADDRESS_FORM': ADDRESS_FORM, + 'gmaps_populate_address': gmaps_populate_address, + 'gmaps_populate_places': gmaps_populate_places, + 'gmaps_get_geolocation': gmaps_get_geolocation, + 'fetchValues': fetchValues, + 'fetchCountryState': fetchCountryState + } +}); \ No newline at end of file diff --git a/ext/custom-addons/web_google_maps/static/src/less/web_maps.less b/ext/custom-addons/web_google_maps/static/src/less/web_maps.less new file mode 100644 index 00000000..7638ea8c --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/less/web_maps.less @@ -0,0 +1,101 @@ +/* https://github.com/twbs/bootstrap/issues/4160 */ +/* put google places autocomplete dropdown results above the bootstrap modal 1050 zindex. */ +.pac-container { + z-index: 1051 !important; +} + +.o_field_x2many_map { + margin-top: 10px; +} + +.o_map_container { + min-height: 400px; + min-width: 100%; + height: 100%; + .o-flex-display(); + + .o_map_view { + .o-flex(1, 1, auto); + min-width: 0; + } + + .o_map_left_sidenav { + left: 0; + top: 0; + bottom: 0; + max-width: 400px; + width: 400px; + position: absolute; + z-index: 1; + background-color: #f5f5f5; + overflow-x: hidden; + transition: 0.5s; + transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); + + &.closed { + transform: translate3d(-100%, 0, 0); + -webkit-transform: translate3d(-100%, 0, 0); + + .sidenav-body .panel { + border: none; + } + } + } + + .o_map_right_sidebar { + right: 0; + bottom: 0; + background-color: #f5f5f5; + + &.closed { + width: 0; + transition: 0.5s; + transform: translateX(-100%); + -webkit-transform: translateX(-100%); + } + + &.open { + width: 250px; + transition: 0.5s; + transform: translateX(0); + -webkit-transform: translateX(0); + overflow-x: auto; + } + + ul { + margin-top: 10px; + padding-left: 3px; + list-style: none; + font-size: 12px; + + li { + padding-bottom: 8px; + + span { + padding: 3px 6px; + + &.total { + font-size: 10px; + background: #ddd; + border-radius: 3px; + } + } + } + } + + } + + .o_map_button { + cursor: pointer; + text-align: center; + width: auto; + padding: 0px 6px; + margin: 0px 15px 5px 0px; + background-color: #ffffff; + border-radius: 2px; + line-height: 25px; + font-family: 'Roboto,Arial,sans-serif'; + } + +} diff --git a/ext/custom-addons/web_google_maps/static/src/xml/widget_places.xml b/ext/custom-addons/web_google_maps/static/src/xml/widget_places.xml new file mode 100644 index 00000000..dd941114 --- /dev/null +++ b/ext/custom-addons/web_google_maps/static/src/xml/widget_places.xml @@ -0,0 +1,30 @@ + + diff --git a/ext/custom-addons/web_google_maps/views/google_places_template.xml b/ext/custom-addons/web_google_maps/views/google_places_template.xml new file mode 100644 index 00000000..d2b31d28 --- /dev/null +++ b/ext/custom-addons/web_google_maps/views/google_places_template.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/ext/custom-addons/web_google_maps/views/res_config.xml b/ext/custom-addons/web_google_maps/views/res_config.xml new file mode 100644 index 00000000..b84124b4 --- /dev/null +++ b/ext/custom-addons/web_google_maps/views/res_config.xml @@ -0,0 +1,74 @@ + + + + view.web.google.config.settings + res.config.settings + + + +
+

Google Maps View

+
+
+
+
+
+
+

Google Maps Libraries

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/ext/custom-addons/web_google_maps/views/res_partner.xml b/ext/custom-addons/web_google_maps/views/res_partner.xml new file mode 100644 index 00000000..7e1c1ef4 --- /dev/null +++ b/ext/custom-addons/web_google_maps/views/res_partner.xml @@ -0,0 +1,104 @@ + + + + view.res.partner.map + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+
    +
  • + +
  • +
  • + +
  • +
  • + at +
  • +
  • + +
  • +
  • + +
  • +
  • + + , +
  • +
  • + +
  • +
+ +
+ + + + + + + kanban,tree,form,map + + + + map + + + +