Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve Google attributions automatically via their API and provide an example showing how to add the Google logo #15598

Merged
merged 16 commits into from Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/google.html
Expand Up @@ -5,10 +5,11 @@
docs: >
This example demonstrates how to display tiles from Google's Map Tiles API in a map. To use the Google Map Tiles API,
you need to set up a Google Cloud project and create an API key for your application. See the
[Map Tiles API documentation](https://developers.google.com/maps/documentation/tile/overview) for instructions.
[Map Tiles API documentation](https://developers.google.com/maps/documentation/tile/overview) for instructions.<br>
The `ol/source/Google` source can be used with a tile layer and is configured by passing properties to the constructor
that are used in creating the [session token request](https://developers.google.com/maps/documentation/tile/session_tokens)
for accessing the tiles. The `mapType` defaults to `'roadmap'` and can be changed to any of the supported map types.
for accessing the tiles. The `mapType` defaults to `'roadmap'` and can be changed to any of the [supported map types](https://developers.google.com/maps/documentation/tile/session_tokens#required_fields).<br>
When using the Google source, please make sure you comply with the Google [Map Tiles API Policies](https://developers.google.com/maps/documentation/tile/policies), by adding the [Google logo](https://developers.google.com/maps/documentation/tile/policies#logo) in the bottom-left corner of the map. You can add the logo as a static image by using a custom OpenLayers control, as shown in the example below.
tags: "google"
---
<div id="map" class="map">
Expand Down
17 changes: 17 additions & 0 deletions examples/google.js
Expand Up @@ -2,6 +2,7 @@ import Google from '../src/ol/source/Google.js';
import Layer from '../src/ol/layer/WebGLTile.js';
import Map from '../src/ol/Map.js';
import View from '../src/ol/View.js';
import {Control, defaults as defaultControls} from '../src/ol/control.js';

function showMap(key) {
const source = new Google({
Expand All @@ -16,8 +17,24 @@ function showMap(key) {
}
});

class GoogleLogoControl 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: element,
});
}
}

const map = new Map({
layers: [new Layer({source})],
controls: defaultControls().extend([new GoogleLogoControl()]),
fnicollet marked this conversation as resolved.
Show resolved Hide resolved
target: 'map',
view: new View({
center: [0, 0],
Expand Down
9 changes: 7 additions & 2 deletions src/ol/control/Attribution.js
Expand Up @@ -6,6 +6,7 @@ import EventType from '../events/EventType.js';
import {CLASS_COLLAPSED, CLASS_CONTROL, CLASS_UNSELECTABLE} from '../css.js';
import {equals} from '../array.js';
import {removeChildren, replaceNode} from '../dom.js';
import {toPromise} from '../functions.js';

/**
* @typedef {Object} Options
Expand Down Expand Up @@ -215,7 +216,7 @@ class Attribution extends Control {
* @private
* @param {?import("../Map.js").FrameState} frameState Frame state.
*/
updateElement_(frameState) {
async updateElement_(frameState) {
if (!frameState) {
if (this.renderedVisible_) {
this.element.style.display = 'none';
Expand All @@ -224,7 +225,11 @@ class Attribution extends Control {
return;
}

const attributions = this.collectSourceAttributions_(frameState);
const attributions = await Promise.all(
this.collectSourceAttributions_(frameState).map((attribution) =>
toPromise(() => attribution),
),
);

const visible = attributions.length > 0;
if (this.renderedVisible_ != visible) {
Expand Down
71 changes: 62 additions & 9 deletions src/ol/source/Google.js
Expand Up @@ -3,16 +3,14 @@
*/

import TileImage from './TileImage.js';
import ViewHint from '../ViewHint.js';
import {createXYZ, extentFromProjection} from '../tilegrid.js';

const defaultAttribution =
'Google Maps' +
'<a class="ol-attribution-google-tos" href="https://cloud.google.com/maps-platform/terms/" target="_blank">Terms of Use</a>' +
' and ' +
'<a class="ol-attribution-google-tos" href="https://policies.google.com/privacy" target="_blank">Privacy Policy</a>';
import {getBottomLeft, getTopRight} from '../extent.js';
import {toLonLat} from '../proj.js';

const createSessionUrl = 'https://tile.googleapis.com/v1/createSession';
const tileUrl = 'https://tile.googleapis.com/v1/2dtiles';
const attributionUrl = 'https://tile.googleapis.com/tile/v1/viewport';
const maxZoom = 22;

/**
Expand All @@ -27,7 +25,6 @@ const maxZoom = 22;
* @property {Array<string>} [layerTypes] The layer types added to the map (e.g. `'layerRoadmap'`, `'layerStreetview'`, or `'layerTraffic'`).
* @property {boolean} [overlay=false] Display only the `layerTypes` and not the underlying `mapType` (only works if `layerTypes` is provided).
* @property {Array<Object>} [styles] [Custom styles](https://developers.google.com/maps/documentation/tile/style-reference) applied to the map.
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {boolean} [interpolate=true] Use interpolated values when resampling. By default,
* linear interpolation is used when resampling. Set to false to use the nearest neighbor instead.
* @property {number} [cacheSize] Initial tile cache size. Will auto-grow to hold at least the number of tiles in the viewport.
Expand Down Expand Up @@ -84,10 +81,9 @@ class Google extends TileImage {
constructor(options) {
const highDpi = !!options.highDpi;
const opaque = !(options.overlay === true);
const attributions = options.attributions || [defaultAttribution];

super({
attributions: attributions,
attributionsCollapsible: false,
cacheSize: options.cacheSize,
crossOrigin: 'anonymous',
interpolate: options.interpolate,
Expand Down Expand Up @@ -146,12 +142,30 @@ class Google extends TileImage {
*/
this.sessionTokenRequest_ = sessionTokenRequest;

/**
* @type {string}
* @private
*/
this.sessionTokenValue_;

/**
* @type {ReturnType<typeof setTimeout>}
* @private
*/
this.sessionRefreshId_;

/**
* @type {string}
* @private
*/
this.previousViewportAttribution_;

/**
* @type {string}
* @private
*/
this.previousViewportExtent_;

this.createSession_();
}

Expand Down Expand Up @@ -225,6 +239,7 @@ class Google extends TileImage {
});

const session = sessionTokenResponse.session;
this.sessionTokenValue_ = session;
const key = this.apiKey_;
this.tileUrlFunction = function (tileCoord, pixelRatio, projection) {
const z = tileCoord[0];
Expand All @@ -238,10 +253,48 @@ class Google extends TileImage {
const timeout = Math.max(expiry - Date.now() - 60 * 1000, 1);
this.sessionRefreshId_ = setTimeout(() => this.createSession_(), timeout);

this.setAttributions(this.fetchAttributions.bind(this));
// even if the state is already ready, we want the change event
this.setState('ready');
}

async fetchAttributions(frameState) {
if (
frameState.viewHints[ViewHint.ANIMATING] ||
frameState.viewHints[ViewHint.INTERACTING] ||
frameState.animate
) {
return this.previousViewportAttribution_;
}
const [west, south] = toLonLat(
getBottomLeft(frameState.extent),
frameState.viewState.projection,
);
const [east, north] = toLonLat(
getTopRight(frameState.extent),
frameState.viewState.projection,
);
const tileGrid = this.getTileGrid();
const zoom = tileGrid.getZForResolution(
frameState.viewState.resolution,
this.zDirection,
);
const viewportExtent = `zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`;
// check if the extent or zoom has actually changed to avoid unnecessary requests
if (this.previousViewportExtent_ == viewportExtent) {
return this.previousViewportAttribution_;
}
this.previousViewportExtent_ = viewportExtent;
fnicollet marked this conversation as resolved.
Show resolved Hide resolved
const session = this.sessionTokenValue_;
const key = this.apiKey_;
const url = `${attributionUrl}?session=${session}&key=${key}&${viewportExtent}`;
this.previousViewportAttribution_ = await fetch(url)
.then((response) => response.json())
.then((json) => json.copyright);

return this.previousViewportAttribution_;
}

disposeInternal() {
clearTimeout(this.sessionRefreshId_);
super.disposeInternal();
Expand Down
11 changes: 8 additions & 3 deletions test/browser/spec/ol/control/attribution.test.js
Expand Up @@ -72,10 +72,15 @@ describe('ol.control.Attribution', function () {
map = null;
});

it('does not add duplicate attributions', function () {
it('does not add duplicate attributions', function (done) {
map.renderSync();
const attribution = map.getTarget().querySelectorAll('.ol-attribution li');
expect(attribution.length).to.be(2);
setTimeout(() => {
const attribution = map
.getTarget()
.querySelectorAll('.ol-attribution li');
expect(attribution.length).to.be(2);
done();
}, 0);
});

it('renders attributions as non-collapsible if source is configured with attributionsCollapsible set to false', function () {
Expand Down