UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

216 lines 9.1 kB
import { logger } from "../../core/logging"; import { Signal0 } from "../../core/signaling"; import { div } from "../../core/dom"; import { wgs84_mercator } from "../../core/util/projections"; import { PlotView } from "./plot_canvas"; function has_maps_API() { return typeof google != "undefined" && typeof google.maps != "undefined"; } const gmaps_ready = new Signal0({}, "gmaps_ready"); const load_google_api = function (api_key, api_version) { window._bokeh_gmaps_callback = () => gmaps_ready.emit(); const enc = encodeURIComponent; const script = document.createElement("script"); script.type = "text/javascript"; script.src = `https://maps.googleapis.com/maps/api/js?v=${enc(api_version)}&key=${enc(api_key)}&callback=_bokeh_gmaps_callback&loading=async`; document.body.appendChild(script); }; export class GMapPlotView extends PlotView { static __name__ = "GMapPlotView"; _tiles_loaded; zoom_count; initial_zoom; initial_lat; initial_lng; map_el; map; map_types; _api_key; initialize() { super.initialize(); this._tiles_loaded = false; this.zoom_count = 0; const { zoom, lat, lng } = this.model.map_options; this.initial_zoom = zoom; this.initial_lat = lat; this.initial_lng = lng; const decoder = new TextDecoder("utf-8"); this._api_key = decoder.decode(this.model.api_key); if (this._api_key == "") { const url = "https://developers.google.com/maps/documentation/javascript/get-api-key"; logger.error(`api_key is required. See ${url} for more information on how to obtain your own.`); } } async lazy_initialize() { await super.lazy_initialize(); this.map_el = div({ style: { position: "absolute" } }); this.canvas_view.underlays_el.append(this.map_el); if (!has_maps_API()) { if (typeof window._bokeh_gmaps_callback === "undefined") { const { api_version } = this.model; load_google_api(this._api_key, api_version); } gmaps_ready.connect(() => { this._build_map(); this.request_repaint(); }); } else { this._build_map(); } } remove() { this.map_el.remove(); super.remove(); } update_range(range_info, options) { // RESET ------------------------- if (range_info == null) { this.map.setCenter({ lat: this.initial_lat, lng: this.initial_lng }); this.map.setOptions({ zoom: this.initial_zoom }); super.reset_range(); // PAN ---------------------------- } else if (range_info.sdx != null || range_info.sdy != null) { this.map.panBy(range_info.sdx ?? 0, range_info.sdy ?? 0); super.update_range(range_info, options); // ZOOM --------------------------- } else if (range_info.factor != null) { // The zoom count decreases the sensitivity of the zoom. (We could make this user configurable) if (this.zoom_count !== 10) { this.zoom_count += 1; return; } this.zoom_count = 0; this.pause(); super.update_range(range_info, options); const zoom_change = range_info.factor < 0 ? -1 : 1; const old_map_zoom = this.map.getZoom(); const bounds = this.map.getBounds(); if (old_map_zoom != null && bounds != null) { const new_map_zoom = old_map_zoom + zoom_change; // Zooming out too far causes problems if (new_map_zoom >= 2) { this.map.setZoom(new_map_zoom); // Check we haven't gone out of bounds, and if we have undo the zoom const [proj_xstart, proj_xend] = this._get_projected_bounds(bounds); if (proj_xend - proj_xstart < 0) { this.map.setZoom(old_map_zoom); } } } this.unpause(); } // Finally re-center this._set_bokeh_ranges(); } _build_map() { const { maps } = google; this.map_types = { satellite: maps.MapTypeId.SATELLITE, terrain: maps.MapTypeId.TERRAIN, roadmap: maps.MapTypeId.ROADMAP, hybrid: maps.MapTypeId.HYBRID, }; const mo = this.model.map_options; const map_options = { center: new maps.LatLng(mo.lat, mo.lng), zoom: mo.zoom, disableDefaultUI: true, mapTypeId: this.map_types[mo.map_type], scaleControl: mo.scale_control, tilt: mo.tilt, }; if (mo.styles != null) { map_options.styles = JSON.parse(mo.styles); } // create the map with above options in div this.map = new maps.Map(this.map_el, map_options); // update bokeh ranges whenever the map idles, which should be after most UI action maps.event.addListener(this.map, "idle", () => this._set_bokeh_ranges()); // also need an event when bounds change so that map resizes trigger renders too maps.event.addListener(this.map, "bounds_changed", () => this._set_bokeh_ranges()); maps.event.addListenerOnce(this.map, "tilesloaded", () => this._render_finished()); // wire up listeners so that changes to properties are reflected this.connect(this.model.properties.map_options.change, () => this._update_options()); this.connect(this.model.map_options.properties.styles.change, () => this._update_styling()); this.connect(this.model.map_options.properties.lat.change, () => this._update_center("lat")); this.connect(this.model.map_options.properties.lng.change, () => this._update_center("lng")); this.connect(this.model.map_options.properties.zoom.change, () => this._update_zoom()); this.connect(this.model.map_options.properties.map_type.change, () => this._update_map_type()); this.connect(this.model.map_options.properties.scale_control.change, () => this._update_scale_control()); this.connect(this.model.map_options.properties.tilt.change, () => this._update_tilt()); } _render_finished() { this._tiles_loaded = true; this.notify_finished(); } has_finished() { return super.has_finished() && this._tiles_loaded === true; } _get_latlon_bounds(bounds) { const top_right = bounds.getNorthEast(); const bottom_left = bounds.getSouthWest(); const xstart = bottom_left.lng(); const xend = top_right.lng(); const ystart = bottom_left.lat(); const yend = top_right.lat(); return [xstart, xend, ystart, yend]; } _get_projected_bounds(bounds) { const [xstart, xend, ystart, yend] = this._get_latlon_bounds(bounds); const [proj_xstart, proj_ystart] = wgs84_mercator.compute(xstart, ystart); const [proj_xend, proj_yend] = wgs84_mercator.compute(xend, yend); return [proj_xstart, proj_xend, proj_ystart, proj_yend]; } _set_bokeh_ranges() { const bounds = this.map.getBounds(); if (bounds != null) { const [proj_xstart, proj_xend, proj_ystart, proj_yend] = this._get_projected_bounds(bounds); this.frame.x_range.setv({ start: proj_xstart, end: proj_xend }); this.frame.y_range.setv({ start: proj_ystart, end: proj_yend }); } } _update_center(fld) { const center = this.map.getCenter()?.toJSON(); if (center != null) { center[fld] = this.model.map_options[fld]; this.map.setCenter(center); this._set_bokeh_ranges(); } } _update_map_type() { this.map.setOptions({ mapTypeId: this.map_types[this.model.map_options.map_type] }); } _update_scale_control() { this.map.setOptions({ scaleControl: this.model.map_options.scale_control }); } _update_tilt() { this.map.setOptions({ tilt: this.model.map_options.tilt }); } _update_options() { this._update_styling(); this._update_center("lat"); this._update_center("lng"); this._update_zoom(); this._update_map_type(); } _update_styling() { const { styles } = this.model.map_options; this.map.setOptions({ styles: styles != null ? JSON.parse(styles) : null }); } _update_zoom() { this.map.setOptions({ zoom: this.model.map_options.zoom }); this._set_bokeh_ranges(); } _after_layout() { super._after_layout(); const { left, top, width, height } = this.frame.bbox; this.map_el.style.top = `${top}px`; this.map_el.style.left = `${left}px`; this.map_el.style.width = `${width}px`; this.map_el.style.height = `${height}px`; } } //# sourceMappingURL=gmap_plot_canvas.js.map