mirror of
https://github.com/getredash/redash.git
synced 2026-03-22 01:00:14 -04:00
303 lines
9.0 KiB
JavaScript
303 lines
9.0 KiB
JavaScript
import _ from 'underscore';
|
|
import d3 from 'd3';
|
|
import angular from 'angular';
|
|
import L from 'leaflet';
|
|
import 'leaflet.markercluster';
|
|
import 'leaflet/dist/leaflet.css';
|
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
|
import markerIcon from 'leaflet/dist/images/marker-icon.png';
|
|
import markerIconRetina from 'leaflet/dist/images/marker-icon-2x.png';
|
|
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
|
|
|
import template from './map.html';
|
|
import editorTemplate from './map-editor.html';
|
|
|
|
/*
|
|
This is a workaround for an issue with giving Leaflet load the icon on its own.
|
|
*/
|
|
L.Icon.Default.mergeOptions({
|
|
iconUrl: markerIcon,
|
|
iconRetinaUrl: markerIconRetina,
|
|
shadowUrl: markerShadow,
|
|
});
|
|
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
|
|
|
|
function mapRenderer() {
|
|
return {
|
|
restrict: 'E',
|
|
template,
|
|
link($scope, elm) {
|
|
const colorScale = d3.scale.category10();
|
|
const map = L.map(elm[0].children[0].children[0], { scrollWheelZoom: false });
|
|
const mapControls = L.control.layers().addTo(map);
|
|
const layers = {};
|
|
const tileLayer = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
|
|
}).addTo(map);
|
|
|
|
function getBounds() {
|
|
$scope.visualization.options.bounds = map.getBounds();
|
|
}
|
|
|
|
function setBounds() {
|
|
const b = $scope.visualization.options.bounds;
|
|
|
|
if (b) {
|
|
map.fitBounds([[b._southWest.lat, b._southWest.lng],
|
|
[b._northEast.lat, b._northEast.lng]]);
|
|
} else if (layers) {
|
|
const allMarkers = _.flatten(_.map(_.values(layers), l => l.getLayers()));
|
|
// eslint-disable-next-line new-cap
|
|
const group = new L.featureGroup(allMarkers);
|
|
map.fitBounds(group.getBounds());
|
|
}
|
|
}
|
|
|
|
|
|
map.on('focus', () => { map.on('moveend', getBounds); });
|
|
map.on('blur', () => { map.off('moveend', getBounds); });
|
|
|
|
function resize() {
|
|
if (!map) return;
|
|
map.invalidateSize(false);
|
|
setBounds();
|
|
}
|
|
|
|
const createMarker = (lat, lon) => L.marker([lat, lon]);
|
|
|
|
const heatpoint = (lat, lon, color) => {
|
|
const style = {
|
|
fillColor: color,
|
|
fillOpacity: 0.9,
|
|
stroke: false,
|
|
};
|
|
|
|
return L.circleMarker([lat, lon], style);
|
|
};
|
|
|
|
function createDescription(latCol, lonCol, row) {
|
|
const lat = row[latCol];
|
|
const lon = row[lonCol];
|
|
|
|
let description = '<ul style="list-style-type: none;padding-left: 0">';
|
|
description += `<li><strong>${lat}, ${lon}</strong>`;
|
|
|
|
_.each(row, (v, k) => {
|
|
if (!(k === latCol || k === lonCol)) {
|
|
description += `<li>${k}: ${v}</li>`;
|
|
}
|
|
});
|
|
|
|
return description;
|
|
}
|
|
|
|
function removeLayer(layer) {
|
|
if (layer) {
|
|
mapControls.removeLayer(layer);
|
|
map.removeLayer(layer);
|
|
}
|
|
}
|
|
|
|
function addLayer(name, points) {
|
|
const latCol = $scope.visualization.options.latColName || 'lat';
|
|
const lonCol = $scope.visualization.options.lonColName || 'lon';
|
|
const classify = $scope.visualization.options.classify;
|
|
|
|
let markers;
|
|
if ($scope.visualization.options.clusterMarkers) {
|
|
const color = $scope.visualization.options.groups[name].color;
|
|
const options = {};
|
|
|
|
if (classify) {
|
|
options.iconCreateFunction = (cluster) => {
|
|
const childCount = cluster.getChildCount();
|
|
|
|
let c = ' marker-cluster-';
|
|
if (childCount < 10) {
|
|
c += 'small';
|
|
} else if (childCount < 100) {
|
|
c += 'medium';
|
|
} else {
|
|
c += 'large';
|
|
}
|
|
|
|
c = '';
|
|
|
|
const style = `color: white; background-color: ${color};`;
|
|
return L.divIcon({ html: `<div style="${style}"><span>${childCount}</span></div>`, className: `marker-cluster${c}`, iconSize: new L.Point(40, 40) });
|
|
};
|
|
}
|
|
|
|
markers = L.markerClusterGroup(options);
|
|
} else {
|
|
markers = L.layerGroup();
|
|
}
|
|
|
|
// create markers
|
|
_.each(points, (row) => {
|
|
let marker;
|
|
|
|
const lat = row[latCol];
|
|
const lon = row[lonCol];
|
|
|
|
if (lat === null || lon === null) return;
|
|
|
|
if (classify && classify !== 'none') {
|
|
const groupColor = $scope.visualization.options.groups[name].color;
|
|
marker = heatpoint(lat, lon, groupColor);
|
|
} else {
|
|
marker = createMarker(lat, lon);
|
|
}
|
|
|
|
marker.bindPopup(createDescription(latCol, lonCol, row));
|
|
markers.addLayer(marker);
|
|
});
|
|
|
|
markers.addTo(map);
|
|
|
|
layers[name] = markers;
|
|
mapControls.addOverlay(markers, name);
|
|
}
|
|
|
|
function render() {
|
|
const queryData = $scope.queryResult.getData();
|
|
const classify = $scope.visualization.options.classify;
|
|
|
|
$scope.visualization.options.mapTileUrl = $scope.visualization.options.mapTileUrl || '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
|
|
|
tileLayer.setUrl($scope.visualization.options.mapTileUrl);
|
|
|
|
if ($scope.visualization.options.clusterMarkers === undefined) {
|
|
$scope.visualization.options.clusterMarkers = true;
|
|
}
|
|
|
|
if (queryData) {
|
|
let pointGroups;
|
|
if (classify && classify !== 'none') {
|
|
pointGroups = _.groupBy(queryData, classify);
|
|
} else {
|
|
pointGroups = { All: queryData };
|
|
}
|
|
|
|
const groupNames = _.keys(pointGroups);
|
|
const options = _.map(groupNames, (group) => {
|
|
if ($scope.visualization.options.groups && $scope.visualization.options.groups[group]) {
|
|
return $scope.visualization.options.groups[group];
|
|
}
|
|
return { color: colorScale(group) };
|
|
});
|
|
|
|
$scope.visualization.options.groups = _.object(groupNames, options);
|
|
|
|
_.each(layers, (v) => {
|
|
removeLayer(v);
|
|
});
|
|
|
|
_.each(pointGroups, (v, k) => {
|
|
addLayer(k, v);
|
|
});
|
|
|
|
setBounds();
|
|
}
|
|
}
|
|
|
|
$scope.$watch('queryResult && queryResult.getData()', render);
|
|
$scope.$watch('visualization.options', render, true);
|
|
angular.element(window).on('resize', resize);
|
|
$scope.$watch('visualization.options.height', resize);
|
|
},
|
|
};
|
|
}
|
|
|
|
function mapEditor() {
|
|
return {
|
|
restrict: 'E',
|
|
template: editorTemplate,
|
|
link($scope) {
|
|
$scope.currentTab = 'general';
|
|
$scope.columns = $scope.queryResult.getColumns();
|
|
$scope.columnNames = _.pluck($scope.columns, 'name');
|
|
$scope.classify_columns = $scope.columnNames.concat('none');
|
|
$scope.mapTiles = [
|
|
{
|
|
name: 'OpenStreetMap',
|
|
url: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenStreetMap BW',
|
|
url: '//{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenStreetMap DE',
|
|
url: '//{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenStreetMap FR',
|
|
url: '//{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenStreetMap Hot',
|
|
url: '//{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'Thunderforest',
|
|
url: '//{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'Thunderforest Spinal',
|
|
url: '//{s}.tile.thunderforest.com/spinal-map/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenMapSurfer',
|
|
url: '//korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}',
|
|
},
|
|
{
|
|
name: 'Stamen Toner',
|
|
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'Stamen Toner Background',
|
|
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'Stamen Toner Lite',
|
|
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
|
|
},
|
|
{
|
|
name: 'OpenTopoMap',
|
|
url: '//{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
|
|
},
|
|
];
|
|
},
|
|
};
|
|
}
|
|
|
|
export default function init(ngModule) {
|
|
ngModule.directive('mapRenderer', mapRenderer);
|
|
ngModule.directive('mapEditor', mapEditor);
|
|
ngModule.config((VisualizationProvider) => {
|
|
const renderTemplate =
|
|
'<map-renderer ' +
|
|
'options="visualization.options" query-result="queryResult">' +
|
|
'</map-renderer>';
|
|
|
|
const editTemplate = '<map-editor></map-editor>';
|
|
const defaultOptions = {
|
|
height: 500,
|
|
classify: 'none',
|
|
clusterMarkers: true,
|
|
};
|
|
|
|
VisualizationProvider.registerVisualization({
|
|
type: 'MAP',
|
|
name: 'Map',
|
|
renderTemplate,
|
|
editorTemplate: editTemplate,
|
|
defaultOptions,
|
|
});
|
|
});
|
|
}
|