From 885d99374cb67d4b9ce9f742aa3664ac58e66fb7 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 2 Dec 2023 12:12:05 +0200 Subject: [PATCH] map providers --- .idea/jsonSchemas.xml | 19 +++ .vscode/settings.json | 6 + .../Espo/Resources/defaults/config.php | 1 + .../Resources/metadata/app/mapProviders.json | 5 + .../Espo/Resources/metadata/fields/map.json | 8 - .../src/handlers/map/google-maps-renderer.js | 160 +++++++++++++++++ client/src/handlers/map/renderer.js | 63 +++++++ client/src/views/fields/map.js | 161 ++++-------------- schema/metadata/app/mapProviders.json | 17 ++ 9 files changed, 302 insertions(+), 138 deletions(-) create mode 100644 application/Espo/Resources/metadata/app/mapProviders.json create mode 100644 client/src/handlers/map/google-maps-renderer.js create mode 100644 client/src/handlers/map/renderer.js create mode 100644 schema/metadata/app/mapProviders.json diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml index b467eac21d..6cc93979a4 100644 --- a/.idea/jsonSchemas.xml +++ b/.idea/jsonSchemas.xml @@ -854,6 +854,25 @@ + + + + + + + diff --git a/.vscode/settings.json b/.vscode/settings.json index cdefcea218..4b205939fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -352,6 +352,12 @@ ], "url": "./schema/metadata/app/entityManager.json" }, + { + "fileMatch": [ + "*/Resources/metadata/app/mapProviders.json" + ], + "url": "./schema/metadata/app/mapProviders.json" + }, { "fileMatch": [ "*/Resources/metadata/app/massActions.json" diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 5e10b8b198..caea07c57a 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -256,6 +256,7 @@ return [ 'newNotificationCountInTitle' => false, 'pdfEngine' => 'Dompdf', 'smsProvider' => null, + 'mapProvider' => 'Google', 'defaultFileStorage' => 'EspoUploadDir', 'ldapUserNameAttribute' => 'sAMAccountName', 'ldapUserFirstNameAttribute' => 'givenName', diff --git a/application/Espo/Resources/metadata/app/mapProviders.json b/application/Espo/Resources/metadata/app/mapProviders.json new file mode 100644 index 0000000000..acd6e70a15 --- /dev/null +++ b/application/Espo/Resources/metadata/app/mapProviders.json @@ -0,0 +1,5 @@ +{ + "Google": { + "renderer": "handlers/map/google-maps-renderer" + } +} diff --git a/application/Espo/Resources/metadata/fields/map.json b/application/Espo/Resources/metadata/fields/map.json index bbf184f924..350af95a3f 100644 --- a/application/Espo/Resources/metadata/fields/map.json +++ b/application/Espo/Resources/metadata/fields/map.json @@ -1,13 +1,5 @@ { "params": [ - { - "name": "provider", - "type": "enum", - "options": [ - "Google" - ], - "default": "Google" - }, { "name": "height", "type": "int", diff --git a/client/src/handlers/map/google-maps-renderer.js b/client/src/handlers/map/google-maps-renderer.js new file mode 100644 index 0000000000..b82a592ebb --- /dev/null +++ b/client/src/handlers/map/google-maps-renderer.js @@ -0,0 +1,160 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import MapRenderer from 'handlers/map/renderer'; + +class GoogleMapsRenderer extends MapRenderer { + + /** + * @param {module:handlers/map/renderer~addressData} addressData + */ + render(addressData) { + if ('google' in window && window.google.maps) { + this.initMapGoogle(addressData); + + return; + } + + // noinspection SpellCheckingInspection + if (typeof window.mapapiloaded === 'function') { + // noinspection SpellCheckingInspection + const mapapiloaded = window.mapapiloaded; + + // noinspection SpellCheckingInspection + window.mapapiloaded = () => { + this.initMapGoogle(addressData); + + mapapiloaded(); + }; + + return; + } + + // noinspection SpellCheckingInspection + window.mapapiloaded = () => this.initMapGoogle(addressData); + + let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded'; + const apiKey = this.view.getConfig().get('googleMapsApiKey'); + + if (apiKey) { + src += '&key=' + apiKey; + } + + const scriptElement = document.createElement('script'); + + scriptElement.setAttribute('async', 'async'); + scriptElement.src = src; + + document.head.appendChild(scriptElement); + } + + /** + * @param {module:handlers/map/renderer~addressData} addressData + */ + initMapGoogle(addressData) { + // noinspection JSUnresolvedReference + const geocoder = new google.maps.Geocoder(); + let map; + + try { + // noinspection SpellCheckingInspection,JSUnresolvedReference + map = new google.maps.Map(this.view.$el.find('.map').get(0), { + zoom: 15, + center: {lat: 0, lng: 0}, + scrollwheel: false, + }); + } + catch (e) { + console.error(e.message); + + return; + } + + let address = ''; + + if (addressData.street) { + address += addressData.street; + } + + if (addressData.city) { + if (address !== '') { + address += ', '; + } + + address += addressData.city; + } + + if (addressData.state) { + if (address !== '') { + address += ', '; + } + + address += addressData.state; + } + + if (addressData.postalCode) { + if (addressData.state || addressData.city) { + address += ' '; + } + else { + if (address) { + address += ', '; + } + } + + address += addressData.postalCode; + } + + if (addressData.country) { + if (address !== '') { + address += ', '; + } + + address += addressData.country; + } + + // noinspection JSUnresolvedReference + geocoder.geocode({'address': address}, (results, status) => { + // noinspection JSUnresolvedReference + if (status === google.maps.GeocoderStatus.OK) { + // noinspection JSUnresolvedReference + map.setCenter(results[0].geometry.location); + + // noinspection JSUnresolvedReference + new google.maps.Marker({ + map: map, + position: results[0].geometry.location, + }); + } + }); + } +} + +// noinspection JSUnusedGlobalSymbols +export default GoogleMapsRenderer; + diff --git a/client/src/handlers/map/renderer.js b/client/src/handlers/map/renderer.js new file mode 100644 index 0000000000..d140ced8bb --- /dev/null +++ b/client/src/handlers/map/renderer.js @@ -0,0 +1,63 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +/** + * @module handlers/map/renderer + */ + +/** + * A map renderer. + * + * @abstract + */ +class MapRenderer { + + /** + * @typedef {Object} module:handlers/map/renderer~addressData + * @property {string|null} street + * @property {string|null} city + * @property {string|null} country + * @property {string|null} state + * @property {string|null} postalCode + */ + + /** + * @param {import('views/fields/map').default} view A field view. + */ + constructor(view) { + this.view = view; + } + + /** + * @param {module:handlers/map/renderer~addressData} addressData + * @abstract + */ + render(addressData) {} +} + +export default MapRenderer; diff --git a/client/src/views/fields/map.js b/client/src/views/fields/map.js index c40c26a46b..06e07a7ea7 100644 --- a/client/src/views/fields/map.js +++ b/client/src/views/fields/map.js @@ -28,10 +28,14 @@ class MapFieldView extends BaseFieldView { detailTemplate = 'fields/map/detail' listTemplate = 'fields/map/detail' - addressField = null - provider = null + /** @type {string} */ + addressField + /** @type {string} */ + provider height = 300 + DEFAULT_PROVIDER = 'Google'; + // noinspection JSCheckFunctionSignatures data() { const data = super.data(); @@ -45,7 +49,7 @@ class MapFieldView extends BaseFieldView { setup() { this.addressField = this.name.slice(0, this.name.length - 3); - this.provider = this.options.provider || this.params.provider; + this.provider = this.provider || this.getConfig().get('mapProvider') || this.DEFAULT_PROVIDER; this.height = this.options.height || this.params.height || this.height; const addressAttributeList = Object.keys(this.getMetadata().get('fields.address.fields') || {}) @@ -93,71 +97,43 @@ class MapFieldView extends BaseFieldView { this.$map = this.$el.find('.map'); if (this.hasAddress()) { - this.processSetHeight(true); - - if (this.height === 'auto') { - $(window).off('resize.' + this.cid); - $(window).on('resize.' + this.cid, this.processSetHeight.bind(this)); - } - - let methodName = 'afterRender' + this.provider.replace(/\s+/g, ''); - - if (typeof this[methodName] === 'function') { - this[methodName](); - } - else { - let implClassName = this.getMetadata() - .get(['clientDefs', 'AddressMap', 'implementations', this.provider]); - - if (implClassName) { - Espo.loader.require(implClassName, impl => { - impl.render(this); - }); - } - } + this.renderMap(); } } - // noinspection JSUnusedGlobalSymbols - afterRenderGoogle() { - if (window.google && window.google.maps) { - this.initMapGoogle(); + renderMap() { + this.processSetHeight(true); + + if (this.height === 'auto') { + $(window).off('resize.' + this.cid); + $(window).on('resize.' + this.cid, this.processSetHeight.bind(this)); + } + + const rendererId = this.getMetadata().get(['app', 'mapProviders', this.provider, 'renderer']); + + if (rendererId) { + Espo.loader.require(rendererId, Renderer => { + (new Renderer(this)).render(this.addressData); + }); return; } - // noinspection SpellCheckingInspection - if (typeof window.mapapiloaded === 'function') { - // noinspection SpellCheckingInspection - let mapapiloaded = window.mapapiloaded; + const methodName = 'afterRender' + this.provider.replace(/\s+/g, ''); - // noinspection SpellCheckingInspection - window.mapapiloaded = () => { - this.initMapGoogle(); - mapapiloaded(); - }; + if (typeof this[methodName] === 'function') { + this[methodName](); return; } - // noinspection SpellCheckingInspection - window.mapapiloaded = () => { - this.initMapGoogle(); - }; + // For bc. + // @todo Remove in v9.0. + const implId = this.getMetadata().get(['clientDefs', 'AddressMap', 'implementations', this.provider]); - let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded'; - let apiKey = this.getConfig().get('googleMapsApiKey'); - - if (apiKey) { - src += '&key=' + apiKey; + if (implId) { + Espo.loader.require(implId, impl => impl.render(this)); } - - let scriptElement = document.createElement('script'); - - scriptElement.setAttribute('async', 'async'); - scriptElement.src = src; - - document.head.appendChild(scriptElement); } processSetHeight(init) { @@ -167,9 +143,7 @@ class MapFieldView extends BaseFieldView { height = this.$el.parent().height(); if (init && height <= 0) { - setTimeout(() => { - this.processSetHeight(true); - }, 50); + setTimeout(() => this.processSetHeight(true), 50); return; } @@ -177,79 +151,6 @@ class MapFieldView extends BaseFieldView { this.$map.css('height', height + 'px'); } - - initMapGoogle() { - const geocoder = new google.maps.Geocoder(); - let map; - - try { - // noinspection SpellCheckingInspection - map = new google.maps.Map(this.$el.find('.map').get(0), { - zoom: 15, - center: {lat: 0, lng: 0}, - scrollwheel: false, - }); - } - catch (e) { - console.error(e.message); - - return; - } - - let address = ''; - - if (this.addressData.street) { - address += this.addressData.street; - } - - if (this.addressData.city) { - if (address !== '') { - address += ', '; - } - - address += this.addressData.city; - } - - if (this.addressData.state) { - if (address !== '') { - address += ', '; - } - - address += this.addressData.state; - } - - if (this.addressData.postalCode) { - if (this.addressData.state || this.addressData.city) { - address += ' '; - } - else { - if (address) { - address += ', '; - } - } - - address += this.addressData.postalCode; - } - - if (this.addressData.country) { - if (address !== '') { - address += ', '; - } - - address += this.addressData.country; - } - - geocoder.geocode({'address': address}, (results, status) => { - if (status === google.maps.GeocoderStatus.OK) { - map.setCenter(results[0].geometry.location); - - new google.maps.Marker({ - map: map, - position: results[0].geometry.location, - }); - } - }); - } } export default MapFieldView; diff --git a/schema/metadata/app/mapProviders.json b/schema/metadata/app/mapProviders.json new file mode 100644 index 0000000000..ee147f186f --- /dev/null +++ b/schema/metadata/app/mapProviders.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.espocrm.com/schema/metadata/app/mapProviders.json", + "title": "app/mapProviders", + "description": "Map providers.", + "type": "object", + "additionalProperties": { + "type": "object", + "description": "A provider name.", + "properties": { + "renderer": { + "type": "string", + "description": "A frontend renderer. Should extend handlers/map/renderer." + } + } + } +}