Geomap: add initial openlayers alpha panel (#36188)

This commit is contained in:
Ryan McKinley
2021-07-09 08:53:07 -07:00
committed by GitHub
parent e4ece0530a
commit 9ce6e2a664
35 changed files with 2173 additions and 36 deletions

View File

@@ -0,0 +1,78 @@
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import XYZ from 'ol/source/XYZ';
import TileLayer from 'ol/layer/Tile';
// https://carto.com/help/building-maps/basemap-list/
export enum LayerTheme {
Auto = 'auto',
Light = 'light',
Dark = 'dark',
}
export interface CartoConfig {
theme?: LayerTheme;
showLabels?: boolean;
}
export const defaultCartoConfig: CartoConfig = {
theme: LayerTheme.Auto,
showLabels: true,
};
export const carto: MapLayerRegistryItem<CartoConfig> = {
id: 'carto',
name: 'CARTO reference map',
isBaseMap: true,
defaultOptions: defaultCartoConfig,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<CartoConfig>, theme: GrafanaTheme2) => ({
init: () => {
const cfg = { ...defaultCartoConfig, ...options.config };
let style = cfg.theme as string;
if (!style || style === LayerTheme.Auto) {
style = theme.isDark ? 'dark' : 'light';
}
if (cfg.showLabels) {
style += '_all';
} else {
style += '_nolabels';
}
return new TileLayer({
source: new XYZ({
attributions: `<a href="https://carto.com/attribution/">© CARTO</a>`,
url: `https://{1-4}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}.png`,
}),
});
},
}),
registerOptionsUI: (builder) => {
builder
.addRadio({
path: 'theme',
name: 'Theme',
settings: {
options: [
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
{ value: LayerTheme.Light, label: 'Light' },
{ value: LayerTheme.Dark, label: 'Dark' },
],
},
defaultValue: defaultCartoConfig.theme!,
})
.addBooleanSwitch({
path: 'showLabels',
name: 'Show labels',
description: '',
defaultValue: defaultCartoConfig.showLabels,
});
},
};
export const cartoLayers = [carto];

View File

@@ -0,0 +1,107 @@
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2, RegistryItem, Registry } from '@grafana/data';
import Map from 'ol/Map';
import { xyzTiles, defaultXYZConfig, XYZConfig } from './generic';
interface PublicServiceItem extends RegistryItem {
slug: string;
}
const CUSTOM_SERVICE = 'custom';
const DEFAULT_SERVICE = 'streets';
export const publicServiceRegistry = new Registry<PublicServiceItem>(() => [
{
id: DEFAULT_SERVICE,
name: 'World Street Map',
slug: 'World_Street_Map',
},
{
id: 'world-imagery',
name: 'World Imagery',
slug: 'World_Imagery',
},
{
id: 'world-physical',
name: 'World Physical',
slug: 'World_Physical_Map',
},
{
id: 'topo',
name: 'Topographic',
slug: 'World_Topo_Map',
},
{
id: 'usa-topo',
name: 'USA Topographic',
slug: 'USA_Topo_Maps',
},
{
id: 'ocean',
name: 'World Ocean',
slug: 'Ocean/World_Ocean_Base',
},
{
id: CUSTOM_SERVICE,
name: 'Custom MapServer',
description: 'Use a custom MapServer with pre-cached values',
slug: '',
},
]);
export interface ESRIXYZConfig extends XYZConfig {
server: string;
}
export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
id: 'esri-xyz',
name: 'ArcGIS MapServer',
isBaseMap: true,
create: (map: Map, options: MapLayerConfig<ESRIXYZConfig>, theme: GrafanaTheme2) => ({
init: () => {
const cfg = { ...options.config };
const svc = publicServiceRegistry.getIfExists(cfg.server ?? DEFAULT_SERVICE)!;
if (svc.id !== CUSTOM_SERVICE) {
const base = 'https://services.arcgisonline.com/ArcGIS/rest/services/';
cfg.url = `${base}${svc.slug}/MapServer/tile/{z}/{y}/{x}`;
cfg.attribution = `Tiles © <a href="${base}${svc.slug}/MapServer">ArcGIS</a>`;
}
// reuse the standard XYZ tile logic
return xyzTiles.create(map, { ...options, config: cfg as XYZConfig }, theme).init();
},
}),
registerOptionsUI: (builder) => {
builder
.addSelect({
path: 'server',
name: 'Server instance',
settings: {
options: publicServiceRegistry.selectOptions().options,
},
})
.addTextInput({
path: 'url',
name: 'URL template',
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
settings: {
placeholder: defaultXYZConfig.url,
},
showIf: (cfg) => cfg.server === CUSTOM_SERVICE,
})
.addTextInput({
path: 'attribution',
name: 'Attribution',
settings: {
placeholder: defaultXYZConfig.attribution,
},
showIf: (cfg) => cfg.server === CUSTOM_SERVICE,
});
},
defaultOptions: {
server: DEFAULT_SERVICE,
},
};
export const esriLayers = [esriXYZTiles];

View File

@@ -0,0 +1,62 @@
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import XYZ from 'ol/source/XYZ';
import TileLayer from 'ol/layer/Tile';
export interface XYZConfig {
url: string;
attribution: string;
minZoom?: number;
maxZoom?: number;
}
const sampleURL = 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer';
export const defaultXYZConfig: XYZConfig = {
url: sampleURL + '/tile/{z}/{y}/{x}',
attribution: `Tiles © <a href="${sampleURL}">ArcGIS</a>`,
};
export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
id: 'xyz',
name: 'XYZ Tile layer',
isBaseMap: true,
create: (map: Map, options: MapLayerConfig<XYZConfig>, theme: GrafanaTheme2) => ({
init: () => {
const cfg = { ...options.config };
if (!cfg.url) {
cfg.url = defaultXYZConfig.url;
cfg.attribution = cfg.attribution ?? defaultXYZConfig.attribution;
}
return new TileLayer({
source: new XYZ({
url: cfg.url,
attributions: cfg.attribution, // singular?
}),
minZoom: cfg.minZoom,
maxZoom: cfg.maxZoom,
});
},
}),
registerOptionsUI: (builder) => {
builder
.addTextInput({
path: 'url',
name: 'URL template',
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
settings: {
placeholder: defaultXYZConfig.url,
},
})
.addTextInput({
path: 'attribution',
name: 'Attribution',
settings: {
placeholder: defaultXYZConfig.attribution,
},
});
},
};
export const genericLayers = [xyzTiles];

View File

@@ -0,0 +1,22 @@
import { cartoLayers, carto } from './carto';
import { esriLayers } from './esri';
import { genericLayers } from './generic';
import { osmLayers } from './osm';
// For now just use carto
export const defaultGrafanaThemedMap = {
...carto,
id: 'default',
name: 'Default base layer',
};
/**
* Registry for layer handlers
*/
export const basemapLayers = [
defaultGrafanaThemedMap,
...osmLayers,
...cartoLayers,
...esriLayers, // keep formatting
...genericLayers,
];

View File

@@ -0,0 +1,24 @@
import { MapLayerRegistryItem, MapLayerConfig } from '@grafana/data';
import Map from 'ol/Map';
import OSM from 'ol/source/OSM';
import TileLayer from 'ol/layer/Tile';
const standard: MapLayerRegistryItem = {
id: 'osm-standard',
name: 'Open Street Map',
isBaseMap: true,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig) => ({
init: () => {
return new TileLayer({
source: new OSM(),
});
},
}),
};
export const osmLayers = [standard];

View File

@@ -0,0 +1,59 @@
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';
export interface GeoJSONMapperConfig {
// URL for a geojson file
src?: string;
// Field name that will map to each featureId
idField?: string;
// Field to use that will set color
valueField?: string;
}
const defaultOptions: GeoJSONMapperConfig = {
src: 'https://openlayers.org/en/latest/examples/data/geojson/countries.geojson',
};
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
id: 'geojson-value-mapper',
name: 'Map values to GeoJSON file',
description: 'color features based on query results',
isBaseMap: false,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<GeoJSONMapperConfig>, theme: GrafanaTheme2): MapLayerHandler => {
const config = { ...defaultOptions, ...options.config };
const source = new VectorSource({
url: config.src,
format: new GeoJSON(),
});
const vectorLayer = new VectorLayer({
source,
});
return {
init: () => vectorLayer,
update: (data: PanelData) => {
console.log( "todo... find values matching the ID and update");
// Update each feature
source.getFeatures().forEach( f => {
console.log( "Find: ", f.getId(), f.getProperties() );
});
},
};
},
// fill in the default values
defaultOptions,
};

View File

@@ -0,0 +1,12 @@
import { geojsonMapper } from './geojsonMapper';
import { lastPointTracker } from './lastPointTracker';
import { worldmapBehaviorLayer } from './worldmapBehavior';
/**
* Registry for layer handlers
*/
export const dataLayers = [
worldmapBehaviorLayer, // mimic the existing worldmap
lastPointTracker,
geojsonMapper, // dummy for now
];

View File

@@ -0,0 +1,78 @@
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, Field, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import * as style from 'ol/style';
import * as source from 'ol/source';
import * as layer from 'ol/layer';
import Point from 'ol/geom/Point';
import { fromLonLat } from 'ol/proj';
export interface LastPointConfig {
icon?: string;
}
const defaultOptions: LastPointConfig = {
icon: 'https://openlayers.org/en/latest/examples/data/icon.png',
};
export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = {
id: 'last-point-tracker',
name: 'Icon at last point',
description: 'Show an icon at the last point',
isBaseMap: false,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<LastPointConfig>, theme: GrafanaTheme2): MapLayerHandler => {
const point = new Feature({});
const config = { ...defaultOptions, ...options.config };
point.setStyle(
new style.Style({
image: new style.Icon({
src: config.icon,
}),
})
);
const vectorSource = new source.Vector({
features: [point],
});
const vectorLayer = new layer.Vector({
source: vectorSource,
});
return {
init: () => vectorLayer,
update: (data: PanelData) => {
const frame = data.series[0];
if (frame && frame.length) {
let lat: Field | undefined = undefined;
let lng: Field | undefined = undefined;
for (const field of frame.fields) {
if (field.name === 'lat') {
lat = field;
} else if (field.name === 'lng') {
lng = field;
}
}
if (lat && lng) {
const idx = lat.values.length - 1;
const latV = lat.values.get(idx);
const lngV = lng.values.get(idx);
if (latV != null && lngV != null) {
point.setGeometry(new Point(fromLonLat([lngV, latV])));
}
}
}
},
};
},
// fill in the default values
defaultOptions,
};

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import * as style from 'ol/style';
import {Point} from 'ol/geom';
import { fromLonLat } from 'ol/proj';
import tinycolor from 'tinycolor2';
import { SimpleLegend } from '../../components/SimpleLegend';
export interface WorldmapConfig {
// anything
}
const defaultOptions: WorldmapConfig = {
// icon: 'https://openlayers.org/en/latest/examples/data/icon.png',
};
export const worldmapBehaviorLayer: MapLayerRegistryItem<WorldmapConfig> = {
id: 'worldmap-behavior',
name: 'Worldmap behavior',
description: 'behave the same as worldmap plugin',
isBaseMap: false,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<WorldmapConfig>, theme: GrafanaTheme2): MapLayerHandler => {
// const config = { ...defaultOptions, ...options.config };
const vectorLayer = new layer.Vector({});
let legendInstance = <SimpleLegend txt={ `initalizing...`}/>;
let count = 0;
return {
init: () => vectorLayer,
legend: () => {
return legendInstance;
},
update: (data: PanelData) => {
count++;
const features:Feature[] = [];
for( let x=0; x<100; x+=20) {
for( let y=0; y<40; y+=10) {
const dot = new Feature({
geometry: new Point(fromLonLat([x,y])),
});
dot.setStyle(new style.Style({
image: new style.Circle({
fill: new style.Fill({
color: tinycolor({r:(x*2), g:(y*3), b:0}).toString(),
}),
radius: (4 + (y*0.5) + (x*0.1)),
})
}));
features.push(dot);
}
}
legendInstance = <SimpleLegend txt={ `Update: ${count}`} data={data}/>;
const vectorSource = new source.Vector({ features });
vectorLayer.setSource(vectorSource);
},
};
},
// fill in the default values
defaultOptions,
};

View File

@@ -0,0 +1,12 @@
import { MapLayerRegistryItem, Registry } from '@grafana/data';
import { basemapLayers } from './basemaps';
import { dataLayers } from './data';
/**
* Registry for layer handlers
*/
export const geomapLayerRegistry = new Registry<MapLayerRegistryItem<any>>(() => [
...basemapLayers, // simple basemaps
...dataLayers, // Layers with update functions
]);