diff --git a/devenv/dev-dashboards/panel-geomap/geomap-photo-layer.json b/devenv/dev-dashboards/panel-geomap/geomap-photo-layer.json new file mode 100644 index 00000000000..08ae930eb4a --- /dev/null +++ b/devenv/dev-dashboards/panel-geomap/geomap-photo-layer.json @@ -0,0 +1,150 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 217, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 22, + "w": 23, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "basemap": { + "config": {}, + "name": "Layer 0", + "type": "default" + }, + "controls": { + "mouseWheelZoom": true, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "border": 2, + "color": "purple", + "crop": true, + "kind": "circle", + "radius": 22, + "shadow": false, + "src": "src" + }, + "location": { + "gazetteer": "public/gazetteer/usa-states.json", + "lookup": "state", + "mode": "lookup" + }, + "name": "Layer 1", + "tooltip": true, + "type": "photos" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "fit", + "lat": 40, + "lon": -100, + "padding": 10, + "zoom": 15 + } + }, + "pluginVersion": "9.3.0-pre", + "targets": [ + { + "csvContent": "state,src\nAL,https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Alabama.svg/320px-Flag_of_Alabama.svg.png\nAK,https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Flag_of_Alaska.svg/320px-Flag_of_Alaska.svg.png\nAZ,https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Flag_of_Arizona.svg/320px-Flag_of_Arizona.svg.png\nAR,https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Flag_of_Arkansas.svg/320px-Flag_of_Arkansas.svg.png\nCA,https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_California.svg/320px-Flag_of_California.svg.png\nCO,https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Flag_of_Colorado.svg/320px-Flag_of_Colorado.svg.png\nCT,https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Flag_of_Connecticut.svg/304px-Flag_of_Connecticut.svg.png\nDE,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/Flag_of_Delaware.svg/320px-Flag_of_Delaware.svg.png\nFL,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Flag_of_Florida.svg/320px-Flag_of_Florida.svg.png\nGA,https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Flag_of_Georgia_%28U.S._state%29.svg/320px-Flag_of_Georgia_%28U.S._state%29.svg.png\nHI,https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Flag_of_Hawaii.svg/320px-Flag_of_Hawaii.svg.png\nID,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Flag_of_Idaho.svg/305px-Flag_of_Idaho.svg.png\nIL,https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Illinois.svg/320px-Flag_of_Illinois.svg.png\nIN,https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Flag_of_Indiana.svg/320px-Flag_of_Indiana.svg.png\nIA,https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Flag_of_Iowa.svg/320px-Flag_of_Iowa.svg.png\nKS,https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Flag_of_Kansas.svg/320px-Flag_of_Kansas.svg.png\nKY,https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Flag_of_Kentucky.svg/320px-Flag_of_Kentucky.svg.png\nLA,https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Flag_of_Louisiana.svg/320px-Flag_of_Louisiana.svg.png\nME,https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Flag_of_the_State_of_Maine.svg/305px-Flag_of_the_State_of_Maine.svg.png\nMD,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Flag_of_Maryland.svg/320px-Flag_of_Maryland.svg.png\nMA,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f2/Flag_of_Massachusetts.svg/320px-Flag_of_Massachusetts.svg.png\nMI,https://upload.wikimedia.org/wikipedia/commons/thumb/b/b5/Flag_of_Michigan.svg/320px-Flag_of_Michigan.svg.png\nMN,https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Flag_of_Minnesota.svg/320px-Flag_of_Minnesota.svg.png\nMS,https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/Flag_of_Mississippi.svg/320px-Flag_of_Mississippi.svg.png\nMO,https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Flag_of_Missouri.svg/320px-Flag_of_Missouri.svg.png\nMT,https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_Montana.svg/320px-Flag_of_Montana.svg.png\nNE,https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Flag_of_Nebraska.svg/320px-Flag_of_Nebraska.svg.png\nNV,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Flag_of_Nevada.svg/320px-Flag_of_Nevada.svg.png\nNH,https://upload.wikimedia.org/wikipedia/commons/thumb/2/28/Flag_of_New_Hampshire.svg/320px-Flag_of_New_Hampshire.svg.png\nNJ,https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_New_Jersey.svg/320px-Flag_of_New_Jersey.svg.png\nNM,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Flag_of_New_Mexico.svg/320px-Flag_of_New_Mexico.svg.png\nNY,https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Flag_of_New_York.svg/320px-Flag_of_New_York.svg.png\nNC,https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Flag_of_North_Carolina.svg/320px-Flag_of_North_Carolina.svg.png\nND,https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Flag_of_North_Dakota.svg/305px-Flag_of_North_Dakota.svg.png\nOH,https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Flag_of_Ohio.svg/320px-Flag_of_Ohio.svg.png\nOK,https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Flag_of_Oklahoma.svg/320px-Flag_of_Oklahoma.svg.png\nOR,https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Flag_of_Oregon.svg/320px-Flag_of_Oregon.svg.png\nPA,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Flag_of_Pennsylvania.svg/320px-Flag_of_Pennsylvania.svg.png\nRI,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Flag_of_Rhode_Island.svg/273px-Flag_of_Rhode_Island.svg.png\nSC,https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/Flag_of_South_Carolina.svg/320px-Flag_of_South_Carolina.svg.png\nSD,https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Flag_of_South_Dakota.svg/320px-Flag_of_South_Dakota.svg.png\nTN,https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Flag_of_Tennessee.svg/320px-Flag_of_Tennessee.svg.png\nTX,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Flag_of_Texas.svg/320px-Flag_of_Texas.svg.png\nUT,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Flag_of_Utah.svg/320px-Flag_of_Utah.svg.png\nVT,https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Vermont.svg/320px-Flag_of_Vermont.svg.png\nVA,https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Flag_of_Virginia.svg/320px-Flag_of_Virginia.svg.png\nWA,https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Flag_of_Washington.svg/320px-Flag_of_Washington.svg.png\nWV,https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Flag_of_West_Virginia.svg/320px-Flag_of_West_Virginia.svg.png\nWI,https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Flag_of_Wisconsin.svg/320px-Flag_of_Wisconsin.svg.png\nWY,https://upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Wyoming.svg/320px-Flag_of_Wyoming.svg.png\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "US State Flags", + "type": "geomap" + } + ], + "schemaVersion": 37, + "style": "dark", + "tags": ["gdev", "panel-tests", "geomap"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - Geomap Photo Layer", + "uid": "Ya_7opHVz", + "version": 14, + "weekStart": "" +} diff --git a/public/app/plugins/panel/geomap/layers/data/index.ts b/public/app/plugins/panel/geomap/layers/data/index.ts index e6c7a8f8273..faa09c12805 100644 --- a/public/app/plugins/panel/geomap/layers/data/index.ts +++ b/public/app/plugins/panel/geomap/layers/data/index.ts @@ -5,6 +5,7 @@ import { lastPointTracker } from './lastPointTracker'; import { routeLayer } from './routeLayer'; import { dayNightLayer } from './dayNightLayer'; import { dynamicGeoJSONLayer } from './geojsonDynamic'; +import { photosLayer } from './photosLayer'; /** * Registry for layer handlers @@ -17,4 +18,5 @@ export const dataLayers = [ dynamicGeoJSONLayer, dayNightLayer, routeLayer, + photosLayer ]; diff --git a/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx b/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx new file mode 100644 index 00000000000..208a9bf4020 --- /dev/null +++ b/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx @@ -0,0 +1,256 @@ +import { + MapLayerRegistryItem, + MapLayerOptions, + PanelData, + GrafanaTheme2, + FrameGeometrySourceMode, + EventBus, + PluginState, + FieldType, + Field, +} from '@grafana/data'; +import Map from 'ol/Map'; +import { FeatureLike } from 'ol/Feature'; +import { getLocationMatchers } from 'app/features/geo/utils/location'; +import VectorLayer from 'ol/layer/Vector'; +import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource'; +import { Stroke, Style } from 'ol/style'; +import Photo from 'ol-ext/style/Photo'; +import { findField } from 'app/features/dimensions'; + +// Configuration options for Circle overlays +export interface PhotoConfig { + kind: 'square' | 'circle' | 'anchored' | 'folio'; + border: number; // Sets border width around images + shadow: boolean; // Renders drop shadow behind images + crop: boolean; // Crops images to fill shape + src?: string; // Image source field + radius: number; // Image radius + color: string; // Border color +} + +const defaultOptions: PhotoConfig = { + kind: 'square', + border: 2, + shadow: true, + crop: true, + radius: 20, + color: 'rgb(200, 200, 200)', +}; + +export const PHOTOS_LAYER_ID = 'photos'; + +// Used by default when nothing is configured +export const defaultPhotosConfig: MapLayerOptions = { + type: PHOTOS_LAYER_ID, + name: '', // will get replaced + config: defaultOptions, + location: { + mode: FrameGeometrySourceMode.Auto, + }, + tooltip: true, +}; + +// TODO Find a way to use SVG scaled to behave like a png, currently using base64 conversion +//const unknownImageSVG = '../../../../../public/img/icons/unicons/question-circle.svg'; +const unknownImage = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBjbGFzcz0iY3NzLWV5eDRkbyI+PHBhdGggZD0iTTExLjI5LDE1LjI5YTEuNTgsMS41OCwwLDAsMC0uMTIuMTUuNzYuNzYsMCwwLDAtLjA5LjE4LjY0LjY0LDAsMCwwLS4wNi4xOCwxLjM2LDEuMzYsMCwwLDAsMCwuMi44NC44NCwwLDAsMCwuMDguMzguOS45LDAsMCwwLC41NC41NC45NC45NCwwLDAsMCwuNzYsMCwuOS45LDAsMCwwLC41NC0uNTRBMSwxLDAsMCwwLDEzLDE2YTEsMSwwLDAsMC0uMjktLjcxQTEsMSwwLDAsMCwxMS4yOSwxNS4yOVpNMTIsMkExMCwxMCwwLDEsMCwyMiwxMiwxMCwxMCwwLDAsMCwxMiwyWm0wLDE4YTgsOCwwLDEsMSw4LThBOCw4LDAsMCwxLDEyLDIwWk0xMiw3QTMsMywwLDAsMCw5LjQsOC41YTEsMSwwLDEsMCwxLjczLDFBMSwxLDAsMCwxLDEyLDlhMSwxLDAsMCwxLDAsMiwxLDEsMCwwLDAtMSwxdjFhMSwxLDAsMCwwLDIsMHYtLjE4QTMsMywwLDAsMCwxMiw3WiI+PC9wYXRoPjwvc3ZnPgo='; +const blankPixel = + 'data:image/svg+xml;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; +let photoLoad: number[] = []; // TODO find a better way to manage this, used to track image load + +/** + * Map layer configuration for circle overlay + */ +export const photosLayer: MapLayerRegistryItem = { + id: PHOTOS_LAYER_ID, + name: 'Photos', + description: 'Render photos at each data point', + isBaseMap: false, + showLocation: true, + hideOpacity: true, + state: PluginState.alpha, + + /** + * Function that configures transformation and returns a transformer + * @param map + * @param options + * @param theme + */ + create: async (map: Map, options: MapLayerOptions, eventBus: EventBus, theme: GrafanaTheme2) => { + // Assert default values + const config = { + ...defaultOptions, + ...options?.config, + }; + + const location = await getLocationMatchers(options.location); + const source = new FrameVectorSource(location); + const vectorLayer = new VectorLayer({ + source, + }); + + let images: string[] = []; + + vectorLayer.setStyle((feature: FeatureLike) => { + let src = unknownImage; + let idx: number = Infinity; + if (images.length > 0) { + idx = feature.get('rowIndex') as number; + src = images[idx] ?? unknownImage; + } + const photoStyle = new Style({ + image: new Photo({ + src, + radius: config.radius, + crop: config.crop, + kind: config.kind, + shadow: false, + stroke: new Stroke({ + width: 0, + color: 'rgba(0,0,0,0)', + }), + onload: () => { + vectorLayer.changed(); // ensure vector layer is rendered properly after image load + if (photoLoad && !photoLoad.includes(idx)) { + photoLoad.push(idx); + } + }, + }), + }); + const blankStyle = new Style({ + image: new Photo({ + src: blankPixel, + radius: config.radius, + crop: false, + kind: config.kind, + shadow: config.shadow, + stroke: new Stroke({ + width: config.border ?? 0, + color: theme.visualization.getColorByName(config.color), + }), + onload: () => { + vectorLayer.changed(); // ensure vector layer is rendered properly after image load + }, + }), + }); + const errorStyle = new Style({ + image: new Photo({ + src: unknownImage, + radius: config.radius, + crop: false, + kind: config.kind, + shadow: false, + stroke: new Stroke({ + width: 0, + color: 'rgba(0,0,0,0)', + }), + onload: () => { + vectorLayer.changed(); // ensure vector layer is rendered properly after image load + }, + }), + }); + if (photoLoad && photoLoad.includes(idx)) { + return [blankStyle, photoStyle]; + } + // If image index is not in the loaded array, return layer stack with error + return [blankStyle, errorStyle, photoStyle]; + }); + + return { + init: () => vectorLayer, + update: (data: PanelData) => { + if (!data.series?.length) { + source.clear(); + return; // ignore empty + } + + for (const frame of data.series) { + source.update(frame); + + // Pick field from config, otherwise use first string field + if (config.src) { + const srcField: Field | undefined = findField(frame, config.src); + if (srcField) { + images = srcField?.values.toArray(); + } + } else { + for (let i = 0; i < frame.fields.length; i++) { + const field = frame.fields[i]; + if (field.type === FieldType.string) { + images = field.values.toArray(); + break; + } + } + } + break; // Only the first frame for now! + } + }, + + // Marker overlay options + registerOptionsUI: (builder) => { + builder + .addFieldNamePicker({ + path: `config.src`, + name: 'Image Source field', + settings: { + filter: (f: Field) => f.type === FieldType.string, + noFieldsMessage: 'No string fields found', + }, + }) + .addRadio({ + path: 'config.kind', + name: 'Kind', + settings: { + options: [ + { label: 'Square', value: 'square' }, + { label: 'Circle', value: 'circle' }, + { label: 'Anchored', value: 'anchored' }, + { label: 'Folio', value: 'folio' }, + ], + }, + defaultValue: defaultOptions.kind, + }) + .addBooleanSwitch({ + path: 'config.crop', + name: 'Crop', + settings: {}, + defaultValue: defaultOptions.crop, + }) + .addBooleanSwitch({ + path: 'config.shadow', + name: 'Shadow', + settings: {}, + defaultValue: defaultOptions.shadow, + }) + .addSliderInput({ + path: 'config.border', + name: 'Border', + settings: { + min: 0, + max: 10, + }, + defaultValue: defaultOptions.border, + }) + .addColorPicker({ + path: 'config.color', + name: 'Border color', + defaultValue: defaultOptions.color, + settings: [{ enableNamedColors: false }], + }) + .addSliderInput({ + path: 'config.radius', + name: 'Radius', + settings: { + min: 1, + max: 100, + }, + defaultValue: defaultOptions.radius, + }); + }, + }; + }, + + // fill in the default values + defaultOptions, +};