From 2fbcc2999b5f34a7439fdf27cba3657c8dd7c48d Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Thu, 27 Oct 2016 15:50:26 -0700 Subject: [PATCH] Port PinMap to Leaflet --- .../metabase/lib/visualization_settings.js | 10 +- .../src/metabase/visualizations/PinMap.jsx | 147 ++++++++++++------ package.json | 2 +- yarn.lock | 6 +- 4 files changed, 111 insertions(+), 54 deletions(-) diff --git a/frontend/src/metabase/lib/visualization_settings.js b/frontend/src/metabase/lib/visualization_settings.js index 0dc02200d88..d1d2e6a8a27 100644 --- a/frontend/src/metabase/lib/visualization_settings.js +++ b/frontend/src/metabase/lib/visualization_settings.js @@ -624,7 +624,6 @@ const SETTINGS = { }), getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region" }, - // TODO: translate legacy settings "map.zoom": { default: 9 }, @@ -633,6 +632,15 @@ const SETTINGS = { }, "map.center_longitude": { default: -122.4376 + }, + "map.pin_type": { + title: "Pin type", + widget: ChartSettingSelect, + props: { + options: [{ name: "Tiles", value: "tiles" }, { name: "Markers", value: "markers" }] + }, + getDefault: (series) => series[0].data.rows.length >= 2000 ? "tiles" : "markers", + getHidden: (series, vizSettings) => vizSettings["map.type"] !== "pin" } }; diff --git a/frontend/src/metabase/visualizations/PinMap.jsx b/frontend/src/metabase/visualizations/PinMap.jsx index 14e4f6e62a9..5db01f95abf 100644 --- a/frontend/src/metabase/visualizations/PinMap.jsx +++ b/frontend/src/metabase/visualizations/PinMap.jsx @@ -4,12 +4,22 @@ import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata"; - import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors"; +import { formatValue } from "metabase/lib/formatting"; + +import "leaflet/dist/leaflet.css"; +import L from "leaflet/dist/leaflet-src.js"; import _ from "underscore"; import cx from "classnames"; +const MARKER_ICON = L.icon({ + iconUrl: "/app/img/pin.png", + iconSize: [28, 32], + iconAnchor: [15, 24], + popupAnchor: [0, -13] +}); + export default class PinMap extends Component { static displayName = "Pin Map"; static identifier = "pin_map"; @@ -91,48 +101,30 @@ export default class PinMap extends Component { const element = ReactDOM.findDOMNode(this.refs.map); const { settings, series: [{ data }] } = this.props; - const mapOptions = { - zoom: settings["map.zoom"], - center: new google.maps.LatLng( - settings["map.center_latitude"], - settings["map.center_longitude"] - ), - mapTypeId: google.maps.MapTypeId.MAP, - scrollwheel: false - }; - - const map = this.map = new google.maps.Map(element, mapOptions); - - if (data.rows.length < 2000) { - let tooltip = new google.maps.InfoWindow(); - let { latitudeIndex, longitudeIndex } = this.getLatLongIndexes(); - for (let row of data.rows) { - let marker = new google.maps.Marker({ - position: new google.maps.LatLng(row[latitudeIndex], row[longitudeIndex]), - map: map, - icon: "/app/img/pin.png" - }); - marker.addListener("click", () => { - let tooltipElement = document.createElement("div"); - ReactDOM.render(<ObjectDetailTooltip row={row} cols={data.cols} />, tooltipElement); - tooltip.setContent(tooltipElement); - tooltip.open(map, marker); - }); - } - } else { - map.overlayMapTypes.push(new google.maps.ImageMapType({ - getTileUrl: this.getTileUrl, - tileSize: new google.maps.Size(256, 256) - })); - } + const map = this.map = L.map(element, { + scrollWheelZoom: false, + }) - map.addListener("center_changed", () => { - let center = map.getCenter(); - this.onMapCenterChange(center.lat(), center.lng()); - }); + map.setView([ + settings["map.center_latitude"], + settings["map.center_longitude"] + ], settings["map.zoom"]); - map.addListener("zoom_changed", () => { - this.onMapZoomChange(map.getZoom()); + L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors' + }).addTo(map); + + this.pinMarkerLayer = L.layerGroup([]); + this.pinTileLayer = L.tileLayer("", {}); + this.popup = L.popup(); + + map.on("moveend", () => { + const { lat, lng } = map.getCenter(); + this.onMapCenterChange(lat, lng); + }); + map.on("zoomend", () => { + const zoom = map.getZoom(); + this.onMapZoomChange(zoom); }); } catch (err) { console.error(err); @@ -140,21 +132,78 @@ export default class PinMap extends Component { } } + _createMarker = (index) => { + const marker = L.marker([0,0], { icon: MARKER_ICON }); + marker.on("click", () => { + const { series: [{ data }] } = this.props; + const { popup } = this; + const el = document.createElement("div"); + ReactDOM.render(<ObjectDetailTooltip row={data.rows[index]} cols={data.cols} />, el); + marker.unbindPopup(); + marker.bindPopup(el, popup); + marker.openPopup(); + }); + return marker; + } + componentDidUpdate() { - if (typeof google !== "undefined") { - google.maps.event.trigger(this.map, "resize"); + try { + const { map, pinTileLayer, pinMarkerLayer } = this; + const { settings, series: [{ data: { rows } }] } = this.props; + const type = settings["map.pin_type"]; + + if (type === "markers") { + const { latitudeIndex, longitudeIndex } = this.getLatLongIndexes(); + let markers = pinMarkerLayer.getLayers(); + let max = Math.max(rows.length, markers.length); + for (let i = 0; i < max; i++) { + if (i >= rows.length) { + pinMarkerLayer.removeLayer(markers[i]); + } + if (i >= markers.length) { + const marker = this._createMarker(i); + pinMarkerLayer.addLayer(marker); + markers.push(marker); + } + + if (i < rows.length) { + const { lat, lng } = markers[i].getLatLng(); + if (lat !== rows[i][latitudeIndex] || lng !== rows[i][longitudeIndex]) { + markers[i].setLatLng([rows[i][latitudeIndex], rows[i][longitudeIndex]]); + } + } + } + + if (!map.hasLayer(pinMarkerLayer)) { + map.removeLayer(pinTileLayer); + map.addLayer(pinMarkerLayer); + } + } else if (type === "tiles") { + const newUrl = this.getTileUrl({ x: "{x}", y: "{y}"}, "{z}"); + if (newUrl !== pinTileLayer._url) { + pinTileLayer.setUrl(newUrl) + } + + if (!map.hasLayer(pinTileLayer)) { + map.removeLayer(pinMarkerLayer); + map.addLayer(pinTileLayer); + } + } + } catch (err) { + console.error(err); + this.props.onRenderError(err.message || err); } } render() { - const { className, isEditing } = this.props; + const { className, isEditing, isDashboard } = this.props; const { lat, lon, zoom } = this.state; const disableUpdateButton = lat == null && lon == null && zoom == null; return ( <div className={className + " PinMap relative"} onMouseDownCapture={(e) =>e.stopPropagation() /* prevent dragging */}> - <div className="absolute top left bottom right" ref="map"></div> - { isEditing ? - <div className={cx("PinMapUpdateButton Button Button--small absolute top right m1", { "PinMapUpdateButton--disabled": disableUpdateButton })} onClick={this.updateSettings}> + <div className="absolute top left bottom right z1" ref="map"></div> + { isEditing || !isDashboard ? + <div className={cx("PinMapUpdateButton Button Button--small absolute top right m1 z2", { "PinMapUpdateButton--disabled": disableUpdateButton })} onClick={this.updateSettings}> Save as default view </div> : null } @@ -168,8 +217,8 @@ const ObjectDetailTooltip = ({ row, cols }) => <tbody> { cols.map((col, index) => <tr> - <td>{col.display_name}</td> - <td>{row[index]}</td> + <td className="pr1">{col.display_name}:</td> + <td>{formatValue(row[index], { column: col, jsx: true })}</td> </tr> )} </tbody> diff --git a/package.json b/package.json index d80fbdf9908..9070e5d5f2e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "inflection": "^1.7.1", "isomorphic-fetch": "^2.2.1", "js-cookie": "^2.1.2", - "leaflet": "^0.7.7", + "leaflet": "^1.0.1", "moment": "2.14.1", "node-libs-browser": "^0.5.3", "normalizr": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 56e18bb0f1b..ae61ae5d518 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3901,9 +3901,9 @@ lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" -leaflet@^0.7.7: - version "0.7.7" - resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-0.7.7.tgz#1e352ba54e63d076451fa363c900890cb2cf75ee" +leaflet: + version "1.0.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.0.1.tgz#5868c389b556cd85481198ff8bfe59a8de0932db" levn@^0.3.0, levn@~0.3.0: version "0.3.0" -- GitLab