From 26b6dbaf71450880813d30ec300d9d790c1dfd60 Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Thu, 27 Mar 2025 16:02:37 -0700 Subject: [PATCH 1/8] Add google map tiles layer --- src/instance/methods/layer.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/instance/methods/layer.js b/src/instance/methods/layer.js index 7f7edde..f9a1780 100644 --- a/src/instance/methods/layer.js +++ b/src/instance/methods/layer.js @@ -1,6 +1,7 @@ // Import source types, layer types, and formats. import VectorSource from 'ol/source/Vector'; import Cluster from 'ol/source/Cluster'; +import Google from 'ol/source/Google'; import TileArcGISRest from 'ol/source/TileArcGISRest'; import TileWMS from 'ol/source/TileWMS'; import XYZ from 'ol/source/XYZ'; @@ -103,6 +104,29 @@ function addGeoJSONLayer({ return layer; } +// Add a Google Map Tiles layer to the map. +function addGoogleMapTilesLayer({ + title = 'google-map-tiles', key, mapType = 'satellite', layerTypes = [], language = 'en-US', region = 'US', apiOptions = null, visible = true, base = true, +}) { + const source = new Google({ + key, + mapType, + layerTypes, + language, + region, + apiOptions, + scale: 'scaleFactor2x', + highDpi: true, + }); + const layer = new TileLayer({ + title, + source, + visible, + type: base ? 'base' : 'normal', + }); + return layer; +} + // Add a Tile ArcGIS MapServer layer to the map. function addTileArcGISMapServerLayer({ title = 'arcgis-tile', url, params, visible = true, base = false, attribution = '', crossOrigin = null, @@ -226,6 +250,12 @@ export default function addLayer(type, opts = {}) { } layer = addGeoJSONLayer(opts); } + if (type.toLowerCase() === 'google') { + if (!opts.key) { + throw new Error('Missing a Google Map Tiles API key.'); + } + layer = addGoogleMapTilesLayer(opts); + } if (type.toLowerCase() === 'arcgis-tile') { if (!opts.url) { throw new Error('Missing a ArcGIS MapServer url.'); From d5c23c876c7e83caf0a2a68f07aafe8d0cde3ab0 Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Mon, 31 Mar 2025 18:58:19 -0700 Subject: [PATCH 2/8] Add GoogleAttribution control --- src/control/Google/GoogleAttribution.js | 64 +++++++++++++++++++++++++ src/instance/defaults.js | 4 ++ 2 files changed, 68 insertions(+) create mode 100644 src/control/Google/GoogleAttribution.js diff --git a/src/control/Google/GoogleAttribution.js b/src/control/Google/GoogleAttribution.js new file mode 100644 index 0000000..ae6db34 --- /dev/null +++ b/src/control/Google/GoogleAttribution.js @@ -0,0 +1,64 @@ +import Control from 'ol/control/Control'; +import Google from 'ol/source/Google'; +import forEachLayer from '../../utils/forEachLayer'; + +/** + * @classdesc + * OpenLayers GoogleAttribution Control. + * + * @api + */ +class GoogleAttribution extends Control { + constructor() { + const element = document.createElement('img'); + element.style.pointerEvents = 'none'; + element.style.position = 'absolute'; + element.style.bottom = '5px'; + element.style.left = '5px'; + element.src = 'https://developers.google.com/static/maps/documentation/images/google_on_white.png'; + super({ + element, + }); + } + + /** + * @inheritDoc + * @api + */ + setMap(map) { + super.setMap(map); + + // Subscribe to the change:visible event on all base layers. + map.on('farmOS-map.layer', () => { + forEachLayer(this.getMap().getLayerGroup(), (layer) => { + if (layer.get('type') === 'base' && typeof layer.getSource === 'function') { + layer.on('change:visible', this.toggle.bind(this)); + } + }); + }); + } + + /** + * Callback to toggle the Google attribution. + * @private + */ + toggle(event) { + + // Only react to layers becoming visible. + if (event.target.get('visible') === true) { + + // If it is a Google layer, display attribution. + const source = event.target.getSource(); + if (source !== null && source instanceof Google) { + this.element.style.display = 'block'; + } + + // Else, hide the Google attribution. + else { + this.element.style.display = 'none'; + } + } + } +} + +export default GoogleAttribution; diff --git a/src/instance/defaults.js b/src/instance/defaults.js index 6764831..7f575bf 100644 --- a/src/instance/defaults.js +++ b/src/instance/defaults.js @@ -31,6 +31,9 @@ import { // Import Geolocate control. import Geolocate from '../control/Geolocate/Geolocate'; +// Import GoogleAttribution control. +import GoogleAttribution from '../control/Google/GoogleAttribution'; + // Define an object that contains the default OpenLayers configuration of layers, // controls, and interactions that will be added to all farmOS maps. const defaults = { @@ -68,6 +71,7 @@ const defaults = { limit: 5, autoComplete: true, }), + new GoogleAttribution(), ]; // If controls were set to 'false', don't attach any controls. From fccded9ccf0bc8a87047bb40106aa2da28c9b773 Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Mon, 31 Mar 2025 19:03:30 -0700 Subject: [PATCH 3/8] Add documentation for adding a google layer --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6f44462..6a6942b 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,16 @@ const wmsOpts = { }; const wmsLayer = myMap.addLayer('wms', wmsOpts); +// Adding a Google Map Tiles layer. +const googleMapOpts = { + title: 'Google Satellite', + key: 'private-api-key', + mapType: 'satellite', + visible: true, // defaults to true + base: true, // defaults to false +} +const googleMapLayer = myMap.addLayer('google', googleMapOpts); + // Adding a ArcGIS MapServer tile layer. const arcGISTileOpts = { title: 'StateCityHighway_USA', // defaults to 'arcgis-tile' From 53d3c2e691e4042d2e0fb73b8d622b90682258bb Mon Sep 17 00:00:00 2001 From: Symbioquine Date: Sat, 31 May 2025 10:57:03 -0700 Subject: [PATCH 4/8] Improve styling and logic for showing Google logo --- src/control/Google/GoogleAttribution.js | 64 ----------------- src/control/Google/GoogleLogoAttribution.css | 18 +++++ src/control/Google/GoogleLogoAttribution.js | 76 ++++++++++++++++++++ src/instance/defaults.js | 4 -- src/instance/methods/layer.js | 7 ++ 5 files changed, 101 insertions(+), 68 deletions(-) delete mode 100644 src/control/Google/GoogleAttribution.js create mode 100644 src/control/Google/GoogleLogoAttribution.css create mode 100644 src/control/Google/GoogleLogoAttribution.js diff --git a/src/control/Google/GoogleAttribution.js b/src/control/Google/GoogleAttribution.js deleted file mode 100644 index ae6db34..0000000 --- a/src/control/Google/GoogleAttribution.js +++ /dev/null @@ -1,64 +0,0 @@ -import Control from 'ol/control/Control'; -import Google from 'ol/source/Google'; -import forEachLayer from '../../utils/forEachLayer'; - -/** - * @classdesc - * OpenLayers GoogleAttribution Control. - * - * @api - */ -class GoogleAttribution extends Control { - constructor() { - const element = document.createElement('img'); - element.style.pointerEvents = 'none'; - element.style.position = 'absolute'; - element.style.bottom = '5px'; - element.style.left = '5px'; - element.src = 'https://developers.google.com/static/maps/documentation/images/google_on_white.png'; - super({ - element, - }); - } - - /** - * @inheritDoc - * @api - */ - setMap(map) { - super.setMap(map); - - // Subscribe to the change:visible event on all base layers. - map.on('farmOS-map.layer', () => { - forEachLayer(this.getMap().getLayerGroup(), (layer) => { - if (layer.get('type') === 'base' && typeof layer.getSource === 'function') { - layer.on('change:visible', this.toggle.bind(this)); - } - }); - }); - } - - /** - * Callback to toggle the Google attribution. - * @private - */ - toggle(event) { - - // Only react to layers becoming visible. - if (event.target.get('visible') === true) { - - // If it is a Google layer, display attribution. - const source = event.target.getSource(); - if (source !== null && source instanceof Google) { - this.element.style.display = 'block'; - } - - // Else, hide the Google attribution. - else { - this.element.style.display = 'none'; - } - } - } -} - -export default GoogleAttribution; diff --git a/src/control/Google/GoogleLogoAttribution.css b/src/control/Google/GoogleLogoAttribution.css new file mode 100644 index 0000000..e02fbe7 --- /dev/null +++ b/src/control/Google/GoogleLogoAttribution.css @@ -0,0 +1,18 @@ +.ol-google-logo-attrib.ol-control { + /* By default position the logo above the scale line and to the right of the snapping grid controls. */ + bottom: 2.75em; + left: 3.5em; + + /* Remove the normal grey control background */ + background-color: rgba(255, 255, 255, 0); +} + +@media (max-width: 767px) { + .map-with-ol-side-panel .ol-google-logo-attrib.ol-control:not(.ol-side-panel) { + /* If the side-panel is expanded on small screens, don't let it get pushed out of view. */ + margin-left: 0; + + /* Add a short transition so it doesn't jump so much. */ + transition: margin-left 500ms; + } +} diff --git a/src/control/Google/GoogleLogoAttribution.js b/src/control/Google/GoogleLogoAttribution.js new file mode 100644 index 0000000..5d67b06 --- /dev/null +++ b/src/control/Google/GoogleLogoAttribution.js @@ -0,0 +1,76 @@ +import Control from 'ol/control/Control'; +import Google from 'ol/source/Google'; +import { CLASS_CONTROL, CLASS_UNSELECTABLE } from 'ol/css'; + +import './GoogleLogoAttribution.css'; + +/** + * @classdesc + * OpenLayers GoogleLogoAttribution Control. + * + * @api + */ +class GoogleLogoAttribution extends Control { + + /** + * @param {Options=} opts GoogleLogoAttribution options. + */ + constructor(opts) { + const options = opts || {}; + + // Call the parent control constructor. + super({ + element: document.createElement('div'), + target: options.target, + }); + + // Define the class name. + const className = 'ol-google-logo-attrib'; + + // Add the CSS classes to the control element. + const { element } = this; + element.className = `${className} ${CLASS_UNSELECTABLE} ${CLASS_CONTROL}`; + + const logoImg = document.createElement('img'); + logoImg.style.pointerEvents = 'none'; + logoImg.src = 'https://developers.google.com/static/maps/documentation/images/google_on_white.png'; + + element.appendChild(logoImg); + } + + /** + * @inheritDoc + * @api + */ + setMap(map) { + const oldMap = this.getMap(); + if (map === oldMap) { + return; + } + if (oldMap) { + + // Cleanup the old event listener + if (this.onMapChangeLayerGroups) { + oldMap.getLayerGroup().un('change', this.onMapChangeLayerGroups.listener); + this.onMapChangeLayerGroups = null; + } + } + super.setMap(map); + + if (map) { + + // Toggle the control visibility upon any top-level layer group change + const updateState = () => { + const anyVisibleGoogleLayers = map.getAllLayers().some(layer => layer.isVisible() && typeof layer.getSource === 'function' && layer.getSource() instanceof Google); + + this.element.style.display = anyVisibleGoogleLayers ? 'block' : 'none'; + }; + + this.onMapChangeLayerGroups = map.getLayerGroup().on('change', updateState); + updateState(); + } + } + +} + +export default GoogleLogoAttribution; diff --git a/src/instance/defaults.js b/src/instance/defaults.js index 7f575bf..6764831 100644 --- a/src/instance/defaults.js +++ b/src/instance/defaults.js @@ -31,9 +31,6 @@ import { // Import Geolocate control. import Geolocate from '../control/Geolocate/Geolocate'; -// Import GoogleAttribution control. -import GoogleAttribution from '../control/Google/GoogleAttribution'; - // Define an object that contains the default OpenLayers configuration of layers, // controls, and interactions that will be added to all farmOS maps. const defaults = { @@ -71,7 +68,6 @@ const defaults = { limit: 5, autoComplete: true, }), - new GoogleAttribution(), ]; // If controls were set to 'false', don't attach any controls. diff --git a/src/instance/methods/layer.js b/src/instance/methods/layer.js index f9a1780..78b7fe6 100644 --- a/src/instance/methods/layer.js +++ b/src/instance/methods/layer.js @@ -20,6 +20,9 @@ import colorStyles, { clusterStyle } from '../../styles'; // Import readFeatures function. import readFeatures from './features'; +// Import GoogleLogoAttribution control. +import GoogleLogoAttribution from '../../control/Google/GoogleLogoAttribution'; + // Set withCredentials to true for all XHR requests made via OpenLayers' // feature loader. Typically farmOS requires authentication in order to // retrieve data from its GeoJSON endpoints. Setting withCredentials to true @@ -255,6 +258,10 @@ export default function addLayer(type, opts = {}) { throw new Error('Missing a Google Map Tiles API key.'); } layer = addGoogleMapTilesLayer(opts); + if (!this.map.getControls().getArray() + .some(control => control instanceof GoogleLogoAttribution)) { + this.map.addControl(new GoogleLogoAttribution()); + } } if (type.toLowerCase() === 'arcgis-tile') { if (!opts.url) { From ae7e4f54c93f8ccb94f5258f5a07f2ee7bf221af Mon Sep 17 00:00:00 2001 From: Jamie Gaehring Date: Thu, 12 Jun 2025 09:32:27 -0400 Subject: [PATCH 5/8] Add further details on Google API keys to README Note that the example API key is the one displayed in the official Google tutorial video, the same one linked in the documentation: https://www.youtube.com/watch?v=2_HZObVbe-g&t=72s --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a6942b..db21b1f 100644 --- a/README.md +++ b/README.md @@ -185,10 +185,14 @@ const wmsOpts = { }; const wmsLayer = myMap.addLayer('wms', wmsOpts); -// Adding a Google Map Tiles layer. +// Adding a Google Map Tiles layer. This requires registering a Google Developer +// account and creating your own private API key. The one below is a fake key +// that won't work but will look similar to your key. Be sure never to share +// yours and if it is accidentally exposed or committed, create a new one. See: +// https://developers.google.com/maps/documentation/javascript/get-api-key const googleMapOpts = { title: 'Google Satellite', - key: 'private-api-key', + key: 'AIzaSyAs-AoPi0VEcL7feKFQRkNpAdjznxqFi3k', // replace with your API key. mapType: 'satellite', visible: true, // defaults to true base: true, // defaults to false From 33582369ca0e9f3d13cfc569882abc44ad2b962f Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Fri, 13 Jun 2025 12:29:44 -0700 Subject: [PATCH 6/8] Update blurb to use map tiles link and language --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db21b1f..545fcf3 100644 --- a/README.md +++ b/README.md @@ -186,10 +186,12 @@ const wmsOpts = { const wmsLayer = myMap.addLayer('wms', wmsOpts); // Adding a Google Map Tiles layer. This requires registering a Google Developer -// account and creating your own private API key. The one below is a fake key -// that won't work but will look similar to your key. Be sure never to share -// yours and if it is accidentally exposed or committed, create a new one. See: -// https://developers.google.com/maps/documentation/javascript/get-api-key +// account and creating your own Map Tiles API key. See the following +// documentation on creating and restricting API keys. +// "Google strongly recommends that you restrict your API keys by limiting their +// usage to those only APIs needed for your application. Restricting API keys +// adds security to your application by protecting it from unwarranted requests." +// https://developers.google.com/maps/documentation/tile/get-api-key const googleMapOpts = { title: 'Google Satellite', key: 'AIzaSyAs-AoPi0VEcL7feKFQRkNpAdjznxqFi3k', // replace with your API key. From 4ba15a3d00fdd108706a7c306dac3fef526e6ba0 Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Fri, 13 Jun 2025 12:33:22 -0700 Subject: [PATCH 7/8] Update example map tiles API key --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 545fcf3..42cd705 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ const wmsLayer = myMap.addLayer('wms', wmsOpts); // https://developers.google.com/maps/documentation/tile/get-api-key const googleMapOpts = { title: 'Google Satellite', - key: 'AIzaSyAs-AoPi0VEcL7feKFQRkNpAdjznxqFi3k', // replace with your API key. + key: 'AIzaSyBEB1xUvz8LG_XexTVKv5ghXEPaCzF9eng', // replace with your API key. mapType: 'satellite', visible: true, // defaults to true base: true, // defaults to false From 1640235b347988ffe20e37885b5264bc9c709d0c Mon Sep 17 00:00:00 2001 From: Paul Weidner Date: Fri, 13 Jun 2025 12:39:16 -0700 Subject: [PATCH 8/8] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b84f7bd..7df3b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add support for Google Map Tiles layers. #207 + ### Changed - Update open layers to version 10.4.0. #206