mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: add initial openlayers alpha panel (#36188)
This commit is contained in:
78
public/app/plugins/panel/geomap/layers/basemaps/carto.ts
Normal file
78
public/app/plugins/panel/geomap/layers/basemaps/carto.ts
Normal 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];
|
||||
107
public/app/plugins/panel/geomap/layers/basemaps/esri.ts
Normal file
107
public/app/plugins/panel/geomap/layers/basemaps/esri.ts
Normal 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];
|
||||
62
public/app/plugins/panel/geomap/layers/basemaps/generic.ts
Normal file
62
public/app/plugins/panel/geomap/layers/basemaps/generic.ts
Normal 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];
|
||||
22
public/app/plugins/panel/geomap/layers/basemaps/index.ts
Normal file
22
public/app/plugins/panel/geomap/layers/basemaps/index.ts
Normal 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,
|
||||
];
|
||||
24
public/app/plugins/panel/geomap/layers/basemaps/osm.ts
Normal file
24
public/app/plugins/panel/geomap/layers/basemaps/osm.ts
Normal 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];
|
||||
59
public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts
Normal file
59
public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts
Normal 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,
|
||||
};
|
||||
12
public/app/plugins/panel/geomap/layers/data/index.ts
Normal file
12
public/app/plugins/panel/geomap/layers/data/index.ts
Normal 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
|
||||
];
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
12
public/app/plugins/panel/geomap/layers/registry.ts
Normal file
12
public/app/plugins/panel/geomap/layers/registry.ts
Normal 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
|
||||
]);
|
||||
Reference in New Issue
Block a user