diff --git a/examples/get-started/pure-js/google-maps/README.md b/examples/get-started/pure-js/google-maps/README.md index fd971637cfb..7327dbde144 100644 --- a/examples/get-started/pure-js/google-maps/README.md +++ b/examples/get-started/pure-js/google-maps/README.md @@ -9,13 +9,14 @@ with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev ## Usage -To run this example, you need a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key). You can either set an environment variable: +To run this example, you need a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key) and a [map id](https://developers.google.com/maps/documentation/javascript/webgl#vector-id). You can either set an environment variable: ```bash export GoogleMapsAPIKey= +export GoogleMapsMapId= ``` -Or set the `GOOGLE_MAPS_API_KEY` variable in `app.js`. +Or set the `GOOGLE_MAPS_API_KEY` and `GOOGLE_MAP_ID` variables in `app.js`. To install dependencies: diff --git a/examples/get-started/pure-js/google-maps/app.js b/examples/get-started/pure-js/google-maps/app.js index 4372a40237a..9d05ba3c122 100644 --- a/examples/get-started/pure-js/google-maps/app.js +++ b/examples/get-started/pure-js/google-maps/app.js @@ -8,7 +8,8 @@ const AIR_PORTS = // Set your Google Maps API key here or via environment variable const GOOGLE_MAPS_API_KEY = process.env.GoogleMapsAPIKey; // eslint-disable-line -const GOOGLE_MAPS_API_URL = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=visualization&v=3.45`; +const GOOGLE_MAP_ID = process.env.GoogleMapsMapId; // eslint-disable-line +const GOOGLE_MAPS_API_URL = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&v=beta&map_ids=${GOOGLE_MAP_ID}`; function loadScript(url) { const script = document.createElement('script'); @@ -24,7 +25,8 @@ function loadScript(url) { loadScript(GOOGLE_MAPS_API_URL).then(() => { const map = new google.maps.Map(document.getElementById('map'), { center: {lat: 51.47, lng: 0.45}, - zoom: 5 + zoom: 5, + mapId: GOOGLE_MAP_ID }); const overlay = new DeckOverlay({ diff --git a/examples/get-started/pure-js/google-maps/package.json b/examples/get-started/pure-js/google-maps/package.json index bbd4b2ca679..583a756a89a 100644 --- a/examples/get-started/pure-js/google-maps/package.json +++ b/examples/get-started/pure-js/google-maps/package.json @@ -8,9 +8,9 @@ "build": "webpack -p" }, "dependencies": { - "@deck.gl/core": "^8.1.0", - "@deck.gl/google-maps": "^8.1.0", - "@deck.gl/layers": "^8.1.0" + "@deck.gl/core": "^8.5.0", + "@deck.gl/google-maps": "^8.5.0", + "@deck.gl/layers": "^8.5.0" }, "devDependencies": { "webpack": "^4.20.2", diff --git a/examples/get-started/pure-js/google-maps/webpack.config.js b/examples/get-started/pure-js/google-maps/webpack.config.js index 75379b7cb88..f25dd757a7d 100644 --- a/examples/get-started/pure-js/google-maps/webpack.config.js +++ b/examples/get-started/pure-js/google-maps/webpack.config.js @@ -12,7 +12,7 @@ const CONFIG = { plugins: [ // Read google maps token from environment variable - new webpack.EnvironmentPlugin(['GoogleMapsAPIKey']) + new webpack.EnvironmentPlugin(['GoogleMapsAPIKey', 'GoogleMapsMapId']) ] }; diff --git a/modules/core/src/views/view.js b/modules/core/src/views/view.js index 11139c84545..487e8890088 100644 --- a/modules/core/src/views/view.js +++ b/modules/core/src/views/view.js @@ -15,11 +15,9 @@ export default class View { height = '100%', // Viewport Options - projectionMatrix = null, // Projection matrix fovy = 50, // Perspective projection parameters, used if projectionMatrix not supplied near = 0.1, // Distance of near clipping plane far = 1000, // Distance of far clipping plane - modelMatrix = null, // A model matrix to be applied to position, to match the layer props API // A View can be a wrapper for a viewport instance viewportInstance = null, @@ -38,11 +36,9 @@ export default class View { this.props = { ...props, id: this.id, - projectionMatrix, fovy, near, - far, - modelMatrix + far }; // Extents diff --git a/modules/google-maps/src/google-maps-overlay.js b/modules/google-maps/src/google-maps-overlay.js index 1ec724b0ff8..5263157828a 100644 --- a/modules/google-maps/src/google-maps-overlay.js +++ b/modules/google-maps/src/google-maps-overlay.js @@ -1,20 +1,25 @@ /* global google */ -import {createDeckInstance, destroyDeckInstance, getViewState} from './utils'; +import {setParameters, withParameters} from '@luma.gl/core'; +import GL from '@luma.gl/constants'; +import { + createDeckInstance, + destroyDeckInstance, + getViewPropsFromOverlay, + getViewPropsFromCoordinateTransformer +} from './utils'; const HIDE_ALL_LAYERS = () => false; +const GL_STATE = { + depthMask: true, + depthTest: true, + blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA], + blendEquation: GL.FUNC_ADD +}; export default class GoogleMapsOverlay { constructor(props) { this.props = {}; - this._map = null; - - const overlay = new google.maps.OverlayView(); - overlay.onAdd = this._onAdd.bind(this); - overlay.onRemove = this._onRemove.bind(this); - overlay.draw = this._draw.bind(this); - this._overlay = overlay; - this.setProps(props); } @@ -30,7 +35,9 @@ export default class GoogleMapsOverlay { } if (map) { this._map = map; - this._overlay.setMap(map); + map.addListener('renderingtype_changed', () => { + this._createOverlay(map); + }); } } @@ -66,18 +73,56 @@ export default class GoogleMapsOverlay { } /* Private API */ + _createOverlay(map) { + const {VECTOR, UNINITIALIZED} = google.maps.RenderingType; + const renderingType = map.getRenderingType(); + if (renderingType === UNINITIALIZED) { + return; + } + const isVectorMap = renderingType === VECTOR; + const OverlayView = isVectorMap ? google.maps.WebglOverlayView : google.maps.OverlayView; + const overlay = new OverlayView(); + + // Lifecycle methods are different depending on map type + if (isVectorMap) { + overlay.onAdd = () => {}; + overlay.onContextLost = this._onContextLost.bind(this); + overlay.onContextRestored = this._onContextRestored.bind(this); + overlay.onDraw = this._onDrawVector.bind(this); + } else { + overlay.onAdd = this._onAdd.bind(this); + overlay.draw = this._onDrawRaster.bind(this); + } + overlay.onRemove = this._onRemove.bind(this); + + this._overlay = overlay; + this._overlay.setMap(map); + } + _onAdd() { this._deck = createDeckInstance(this._map, this._overlay, this._deck, this.props); } + _onContextRestored(gl) { + this._deck = createDeckInstance(this._map, this._overlay, this._deck, {gl, ...this.props}); + } + + _onContextLost() { + // TODO this isn't working + if (this._deck) { + destroyDeckInstance(this._deck); + this._deck = null; + } + } + _onRemove() { // Clear deck canvas this._deck.setProps({layerFilter: HIDE_ALL_LAYERS}); } - _draw() { + _onDrawRaster() { const deck = this._deck; - const {width, height, left, top, zoom, pitch, latitude, longitude} = getViewState( + const {width, height, left, top, zoom, pitch, latitude, longitude} = getViewPropsFromOverlay( this._map, this._overlay ); @@ -98,4 +143,31 @@ export default class GoogleMapsOverlay { // Deck is initialized deck.redraw(); } + + // Vector code path + _onDrawVector(gl, coordinateTransformer) { + const deck = this._deck; + + deck.setProps({ + ...getViewPropsFromCoordinateTransformer(this._map, coordinateTransformer) + }); + + if (deck.layerManager) { + this._overlay.requestRedraw(); + withParameters(gl, GL_STATE, () => { + deck._drawLayers('google-vector', { + clearCanvas: false + }); + }); + + // Reset state otherwise get rendering errors in + // Google library. These occur because the picking + // code is run outside of the _onDrawVector() method and + // the GL state can be inconsistent + setParameters(gl, { + scissor: [0, 0, gl.canvas.width, gl.canvas.height], + stencilFunc: [gl.ALWAYS, 0, 255, gl.ALWAYS, 0, 255] + }); + } + } } diff --git a/modules/google-maps/src/utils.js b/modules/google-maps/src/utils.js index a09ad9f9728..fab747a471b 100644 --- a/modules/google-maps/src/utils.js +++ b/modules/google-maps/src/utils.js @@ -1,5 +1,6 @@ /* global google, document */ import {Deck} from '@deck.gl/core'; +import {Matrix4} from 'math.gl'; // https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas const MAX_LATITUDE = 85.05113; @@ -57,7 +58,17 @@ function getContainer(overlay, style) { const container = document.createElement('div'); container.style.position = 'absolute'; Object.assign(container.style, style); - overlay.getPanes().overlayLayer.appendChild(container); + + // The DOM structure has a different structure depending on whether + // the Google map is rendered as vector or raster + if (overlay.getPanes) { + overlay.getPanes().overlayLayer.appendChild(container); + } else { + overlay + .getMap() + .getDiv() + .appendChild(container); + } return container; } @@ -82,12 +93,8 @@ export function destroyDeckInstance(deck) { * @param map (google.maps.Map) - The parent Map instance * @param overlay (google.maps.OverlayView) - A maps Overlay instance */ -export function getViewState(map, overlay) { - // The map fills the container div unless it's in fullscreen mode - // at which point the first child of the container is promoted - const container = map.getDiv().firstChild; - const width = container.offsetWidth; - const height = container.offsetHeight; +export function getViewPropsFromOverlay(map, overlay) { + const {width, height} = getMapSize(map); // Canvas position relative to draggable map's container depends on // overlayView's projection, not the map's. Have to use the center of the @@ -145,8 +152,67 @@ export function getViewState(map, overlay) { longitude }; } + /* eslint-enable max-statements */ +/** + * Get the current view state + * @param map (google.maps.Map) - The parent Map instance + * @param coordinateTransformer (google.maps.CoordinateTransformer) - A CoordinateTransformer instance + */ +export function getViewPropsFromCoordinateTransformer(map, coordinateTransformer) { + const {width, height} = getMapSize(map); + const { + lat: latitude, + lng: longitude, + heading: bearing, + tilt: pitch, + zoom + } = coordinateTransformer.getCameraParams(); + + // Match Google projection matrix + const fovy = 25; + const aspect = width / height; + + // Match depth range (crucial for correct z-sorting) + const near = 0.75; + const far = 300000000000000; + // const far = Infinity; + + const projectionMatrix = new Matrix4().perspective({ + fovy: (fovy * Math.PI) / 180, + aspect, + near, + far + }); + const focalDistance = 0.5 * projectionMatrix[5]; + + return { + width, + height, + viewState: { + altitude: focalDistance, + bearing, + latitude, + longitude, + pitch, + projectionMatrix, + repeat: true, + zoom: zoom - 1 + } + }; +} + +function getMapSize(map) { + // The map fills the container div unless it's in fullscreen mode + // at which point the first child of the container is promoted + const container = map.getDiv().firstChild; + return { + width: container.offsetWidth, + height: container.offsetHeight + }; +} + function getEventPixel(event, deck) { if (event.pixel) { return event.pixel; diff --git a/test/modules/google-maps/google-maps-overlay.spec.js b/test/modules/google-maps/google-maps-overlay.spec.js index e9868c8a644..2fde9280a3e 100644 --- a/test/modules/google-maps/google-maps-overlay.spec.js +++ b/test/modules/google-maps/google-maps-overlay.spec.js @@ -26,6 +26,7 @@ test('GoogleMapsOverlay#constructor', t => { }); overlay.setMap(map); + map.emit({type: 'renderingtype_changed'}); const deck = overlay._deck; t.ok(deck, 'Deck instance is created'); @@ -41,83 +42,144 @@ test('GoogleMapsOverlay#constructor', t => { t.end(); }); -test('GoogleMapsOverlay#style', t => { +test('GoogleMapsOverlay#raster lifecycle', t => { const map = new mapsApi.Map({ width: 1, height: 1, longitude: 0, latitude: 0, - zoom: 1 + zoom: 1, + renderingType: mapsApi.RenderingType.RASTER }); const overlay = new GoogleMapsOverlay({ - style: {zIndex: 10}, layers: [] }); overlay.setMap(map); - const deck = overlay._deck; + map.emit({type: 'renderingtype_changed'}); + t.ok(overlay._overlay.onAdd, 'onAdd lifecycle function is registered'); + t.ok(overlay._overlay.draw, 'draw lifecycle function is registered'); + t.ok(overlay._overlay.onRemove, 'onRemove lifecycle function is registered'); + overlay.finalize(); - t.is(deck.props.parent.style.zIndex, '10', 'parent zIndex is set'); - t.is(deck.canvas.style.zIndex, '', 'canvas zIndex is not set'); + t.end(); +}); - overlay.setProps({ - style: {zIndex: 5} +test('GoogleMapsOverlay#vector lifecycle', t => { + const map = new mapsApi.Map({ + width: 1, + height: 1, + longitude: 0, + latitude: 0, + zoom: 1, + renderingType: mapsApi.RenderingType.VECTOR + }); + + const overlay = new GoogleMapsOverlay({ + layers: [] }); - t.is(deck.props.parent.style.zIndex, '5', 'parent zIndex is set'); - t.is(deck.canvas.style.zIndex, '', 'canvas zIndex is not set'); + overlay.setMap(map); + map.emit({type: 'renderingtype_changed'}); + t.ok(overlay._overlay.onAdd, 'onAdd lifecycle function is registered'); + t.ok(overlay._overlay.onContextLost, 'onContextLost lifecycle function is registered'); + t.ok(overlay._overlay.onContextRestored, 'onContextRestored lifecycle function is registered'); + t.ok(overlay._overlay.onDraw, 'onDraw lifecycle function is registered'); + t.ok(overlay._overlay.onRemove, 'onRemove lifecycle function is registered'); overlay.finalize(); t.end(); }); -test('GoogleMapsOverlay#draw, pick', t => { +test('GoogleMapsOverlay#style', t => { const map = new mapsApi.Map({ - width: 800, - height: 400, - longitude: -122.45, - latitude: 37.78, - zoom: 13 + width: 1, + height: 1, + longitude: 0, + latitude: 0, + zoom: 1 }); const overlay = new GoogleMapsOverlay({ - layers: [ - new ScatterplotLayer({ - data: [{position: [0, 0]}, {position: [0, 0]}], - radiusMinPixels: 100, - pickable: true - }) - ] + style: {zIndex: 10}, + layers: [] }); overlay.setMap(map); + map.emit({type: 'renderingtype_changed'}); const deck = overlay._deck; - t.notOk(deck.props.viewState, 'Deck does not have view state'); - - map.draw(); - const {viewState, width, height} = deck.props; - t.ok(equals(viewState.longitude, map.opts.longitude), 'longitude is set'); - t.ok(equals(viewState.latitude, map.opts.latitude), 'latitude is set'); - t.ok(equals(viewState.zoom, map.opts.zoom - 1), 'zoom is set'); - t.ok(equals(width, map.opts.width), 'width is set'); - t.ok(equals(height, map.opts.height), 'height is set'); - t.notOk(deck.props.layerFilter, 'layerFilter is empty'); - - map.setTilt(45); - map.draw(); - t.ok(deck.props.layerFilter, 'layerFilter should be set to block drawing'); - - const pointerMoveSpy = makeSpy(overlay._deck, '_onPointerMove'); - map.emit({type: 'mousemove', pixel: [0, 0]}); - t.is(pointerMoveSpy.callCount, 1, 'pointer move event is handled'); + t.is(deck.props.parent.style.zIndex, '10', 'parent zIndex is set'); + t.is(deck.canvas.style.zIndex, '', 'canvas zIndex is not set'); - map.emit({type: 'mouseout', pixel: [0, 0]}); - t.is(pointerMoveSpy.callCount, 2, 'pointer leave event is handled'); - pointerMoveSpy.reset(); + overlay.setProps({ + style: {zIndex: 5} + }); + t.is(deck.props.parent.style.zIndex, '5', 'parent zIndex is set'); + t.is(deck.canvas.style.zIndex, '', 'canvas zIndex is not set'); overlay.finalize(); t.end(); }); + +function drawPickTest(renderingType) { + test(`GoogleMapsOverlay#draw, pick ${renderingType}`, t => { + const map = new mapsApi.Map({ + width: 800, + height: 400, + longitude: -122.45, + latitude: 37.78, + zoom: 13, + renderingType + }); + + const overlay = new GoogleMapsOverlay({ + layers: [ + new ScatterplotLayer({ + data: [{position: [0, 0]}, {position: [0, 0]}], + radiusMinPixels: 100, + pickable: true + }) + ] + }); + + overlay.setMap(map); + map.emit({type: 'renderingtype_changed'}); + const deck = overlay._deck; + + t.notOk(deck.props.viewState, 'Deck does not have view state'); + + map.draw(); + const {viewState, width, height} = deck.props; + t.ok(equals(viewState.longitude, map.opts.longitude), 'longitude is set'); + t.ok(equals(viewState.latitude, map.opts.latitude), 'latitude is set'); + t.ok(equals(viewState.zoom, map.opts.zoom - 1), 'zoom is set'); + t.ok(equals(width, map.opts.width), 'width is set'); + t.ok(equals(height, map.opts.height), 'height is set'); + + if (renderingType === mapsApi.RenderingType.RASTER) { + t.notOk(deck.props.layerFilter, 'layerFilter is empty'); + + map.setTilt(45); + map.draw(); + t.ok(deck.props.layerFilter, 'layerFilter should be set to block drawing'); + } + + const pointerMoveSpy = makeSpy(overlay._deck, '_onPointerMove'); + map.emit({type: 'mousemove', pixel: [0, 0]}); + t.is(pointerMoveSpy.callCount, 1, 'pointer move event is handled'); + + map.emit({type: 'mouseout', pixel: [0, 0]}); + t.is(pointerMoveSpy.callCount, 2, 'pointer leave event is handled'); + pointerMoveSpy.reset(); + + overlay.finalize(); + + t.end(); + }); +} +for (const renderingType of [mapsApi.RenderingType.RASTER, mapsApi.RenderingType.VECTOR]) { + drawPickTest(renderingType); +} diff --git a/test/modules/google-maps/mock-maps-api.js b/test/modules/google-maps/mock-maps-api.js index ae4ff2bff4e..2025feee577 100644 --- a/test/modules/google-maps/mock-maps-api.js +++ b/test/modules/google-maps/mock-maps-api.js @@ -1,5 +1,11 @@ import {WebMercatorViewport} from '@deck.gl/core'; +export const RenderingType = { + VECTOR: 'VECTOR', + RASTER: 'RASTER', + UNINITIALIZED: 'UNINITIALIZED' +}; + export class Point { constructor(x, y) { this.x = x; @@ -74,6 +80,17 @@ export class Map { this._callbacks = {}; this.projection = new Projection(opts); + this.coordinateTransformer = { + getCameraParams: () => { + return { + lat: this.opts.latitude, + lng: this.opts.longitude, + heading: this.getHeading(), + tilt: this.getTilt(), + zoom: this.getZoom() + }; + } + }; } addListener(event, cb) { @@ -93,9 +110,15 @@ export class Map { } } + getRenderingType() { + return this.opts.renderingType || RenderingType.RASTER; + } + draw() { for (const overlay of this._overlays) { - overlay.draw(); + this.getRenderingType() === RenderingType.RASTER + ? overlay.draw() + : overlay.onDraw(undefined, this.coordinateTransformer); } } @@ -116,6 +139,14 @@ export class Map { return this.opts.zoom; } + setHeading(heading) { + this.opts.heading = heading; + } + + getHeading() { + return this.opts.heading || 0; + } + setTilt(tilt) { this.opts.pitch = tilt; } @@ -127,6 +158,9 @@ export class Map { _addOverlay(overlay) { this._overlays.add(overlay); overlay.onAdd(); + if (this.getRenderingType() === RenderingType.VECTOR) { + overlay.onContextRestored(); + } } _removeOverlay(overlay) { @@ -161,3 +195,22 @@ export class OverlayView { }; } } + +export class WebglOverlayView { + constructor() { + this.map = null; + this._container = document.createElement('div'); + } + + setMap(map) { + this.map?._removeOverlay(this); + map?._addOverlay(this); + this.map = map; + } + + getMap() { + return { + getDiv: () => this._container + }; + } +}