1import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style'; 2import { Registry, RegistryItem } from '@grafana/data'; 3import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types'; 4import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; 5import tinycolor from 'tinycolor2'; 6import { config } from '@grafana/runtime'; 7 8interface SymbolMaker extends RegistryItem { 9 aliasIds: string[]; 10 make: StyleMaker; 11} 12 13enum RegularShapeId { 14 circle = 'circle', 15 square = 'square', 16 triangle = 'triangle', 17 star = 'star', 18 cross = 'cross', 19 x = 'x', 20} 21 22const MarkerShapePath = { 23 circle: 'img/icons/marker/circle.svg', 24 square: 'img/icons/marker/square.svg', 25 triangle: 'img/icons/marker/triangle.svg', 26 star: 'img/icons/marker/star.svg', 27 cross: 'img/icons/marker/cross.svg', 28 x: 'img/icons/marker/x-mark.svg', 29}; 30 31export function getFillColor(cfg: StyleConfigValues) { 32 const opacity = cfg.opacity == null ? 0.8 : cfg.opacity; 33 if (opacity === 1) { 34 return new Fill({ color: cfg.color }); 35 } 36 if (opacity > 0) { 37 const color = tinycolor(cfg.color).setAlpha(opacity).toRgbString(); 38 return new Fill({ color }); 39 } 40 return undefined; 41} 42 43const textLabel = (cfg: StyleConfigValues) => { 44 if (!cfg.text) { 45 return undefined; 46 } 47 48 const fontFamily = config.theme2.typography.fontFamily; 49 const textConfig = { 50 ...defaultStyleConfig.textConfig, 51 ...cfg.textConfig, 52 }; 53 return new Text({ 54 text: cfg.text, 55 fill: new Fill({ color: cfg.color ?? defaultStyleConfig.color.fixed }), 56 font: `normal ${textConfig.fontSize}px ${fontFamily}`, 57 ...textConfig, 58 }); 59}; 60 61export const textMarker = (cfg: StyleConfigValues) => { 62 return new Style({ 63 text: textLabel(cfg), 64 }); 65}; 66 67export const circleMarker = (cfg: StyleConfigValues) => { 68 return new Style({ 69 image: new Circle({ 70 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 71 fill: getFillColor(cfg), 72 radius: cfg.size ?? DEFAULT_SIZE, 73 }), 74 text: textLabel(cfg), 75 }); 76}; 77 78export const polyStyle = (cfg: StyleConfigValues) => { 79 return new Style({ 80 fill: getFillColor(cfg), 81 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 82 text: textLabel(cfg), 83 }); 84}; 85 86// Square and cross 87const errorMarker = (cfg: StyleConfigValues) => { 88 const radius = cfg.size ?? DEFAULT_SIZE; 89 const stroke = new Stroke({ color: '#F00', width: 1 }); 90 return [ 91 new Style({ 92 image: new RegularShape({ 93 stroke, 94 points: 4, 95 radius, 96 angle: Math.PI / 4, 97 }), 98 }), 99 new Style({ 100 image: new RegularShape({ 101 stroke, 102 points: 4, 103 radius, 104 radius2: 0, 105 angle: 0, 106 }), 107 }), 108 ]; 109}; 110 111const makers: SymbolMaker[] = [ 112 { 113 id: RegularShapeId.circle, 114 name: 'Circle', 115 aliasIds: [MarkerShapePath.circle], 116 make: circleMarker, 117 }, 118 { 119 id: RegularShapeId.square, 120 name: 'Square', 121 aliasIds: [MarkerShapePath.square], 122 make: (cfg: StyleConfigValues) => { 123 const radius = cfg.size ?? DEFAULT_SIZE; 124 const rotation = cfg.rotation ?? 0; 125 return new Style({ 126 image: new RegularShape({ 127 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 128 fill: getFillColor(cfg), 129 points: 4, 130 radius, 131 rotation: (rotation * Math.PI) / 180 + Math.PI / 4, 132 }), 133 text: textLabel(cfg), 134 }); 135 }, 136 }, 137 { 138 id: RegularShapeId.triangle, 139 name: 'Triangle', 140 aliasIds: [MarkerShapePath.triangle], 141 make: (cfg: StyleConfigValues) => { 142 const radius = cfg.size ?? DEFAULT_SIZE; 143 const rotation = cfg.rotation ?? 0; 144 return new Style({ 145 image: new RegularShape({ 146 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 147 fill: getFillColor(cfg), 148 points: 3, 149 radius, 150 rotation: (rotation * Math.PI) / 180, 151 angle: 0, 152 }), 153 text: textLabel(cfg), 154 }); 155 }, 156 }, 157 { 158 id: RegularShapeId.star, 159 name: 'Star', 160 aliasIds: [MarkerShapePath.star], 161 make: (cfg: StyleConfigValues) => { 162 const radius = cfg.size ?? DEFAULT_SIZE; 163 const rotation = cfg.rotation ?? 0; 164 return new Style({ 165 image: new RegularShape({ 166 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 167 fill: getFillColor(cfg), 168 points: 5, 169 radius, 170 radius2: radius * 0.4, 171 angle: 0, 172 rotation: (rotation * Math.PI) / 180, 173 }), 174 text: textLabel(cfg), 175 }); 176 }, 177 }, 178 { 179 id: RegularShapeId.cross, 180 name: 'Cross', 181 aliasIds: [MarkerShapePath.cross], 182 make: (cfg: StyleConfigValues) => { 183 const radius = cfg.size ?? DEFAULT_SIZE; 184 const rotation = cfg.rotation ?? 0; 185 return new Style({ 186 image: new RegularShape({ 187 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 188 points: 4, 189 radius, 190 radius2: 0, 191 angle: 0, 192 rotation: (rotation * Math.PI) / 180, 193 }), 194 text: textLabel(cfg), 195 }); 196 }, 197 }, 198 { 199 id: RegularShapeId.x, 200 name: 'X', 201 aliasIds: [MarkerShapePath.x], 202 make: (cfg: StyleConfigValues) => { 203 const radius = cfg.size ?? DEFAULT_SIZE; 204 const rotation = cfg.rotation ?? 0; 205 return new Style({ 206 image: new RegularShape({ 207 stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), 208 points: 4, 209 radius, 210 radius2: 0, 211 rotation: (rotation * Math.PI) / 180 + Math.PI / 4, 212 }), 213 text: textLabel(cfg), 214 }); 215 }, 216 }, 217]; 218 219async function prepareSVG(url: string, size?: number): Promise<string> { 220 return fetch(url, { method: 'GET' }) 221 .then((res) => { 222 return res.text(); 223 }) 224 .then((text) => { 225 const parser = new DOMParser(); 226 const doc = parser.parseFromString(text, 'image/svg+xml'); 227 const svg = doc.getElementsByTagName('svg')[0]; 228 if (!svg) { 229 return ''; 230 } 231 232 const svgSize = size ?? 100; 233 const width = svg.getAttribute('width') ?? svgSize; 234 const height = svg.getAttribute('height') ?? svgSize; 235 236 // open layers requires a white fill becaues it uses tint to set color 237 svg.setAttribute('fill', '#fff'); 238 svg.setAttribute('width', `${width}px`); 239 svg.setAttribute('height', `${height}px`); 240 const svgString = new XMLSerializer().serializeToString(svg); 241 const svgURI = encodeURIComponent(svgString); 242 return `data:image/svg+xml,${svgURI}`; 243 }) 244 .catch((error) => { 245 console.error(error); 246 return ''; 247 }); 248} 249 250// Really just a cache for the various symbol styles 251const markerMakers = new Registry<SymbolMaker>(() => makers); 252 253export function getMarkerAsPath(shape?: string): string | undefined { 254 const marker = markerMakers.getIfExists(shape); 255 if (marker?.aliasIds?.length) { 256 return marker.aliasIds[0]; 257 } 258 return undefined; 259} 260 261// Will prepare symbols as necessary 262export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): Promise<StyleMaker> { 263 if (!symbol) { 264 return hasTextLabel ? textMarker : circleMarker; 265 } 266 267 let maker = markerMakers.getIfExists(symbol); 268 if (maker) { 269 return maker.make; 270 } 271 272 // Prepare svg as icon 273 if (symbol.endsWith('.svg')) { 274 const src = await prepareSVG(getPublicOrAbsoluteUrl(symbol)); 275 maker = { 276 id: symbol, 277 name: symbol, 278 aliasIds: [], 279 make: src 280 ? (cfg: StyleConfigValues) => { 281 const radius = cfg.size ?? DEFAULT_SIZE; 282 const rotation = cfg.rotation ?? 0; 283 return [ 284 new Style({ 285 image: new Icon({ 286 src, 287 color: cfg.color, 288 opacity: cfg.opacity ?? 1, 289 scale: (DEFAULT_SIZE + radius) / 100, 290 rotation: (rotation * Math.PI) / 180, 291 }), 292 text: !cfg?.text ? undefined : textLabel(cfg), 293 }), 294 // transparent bounding box for featureAtPixel detection 295 new Style({ 296 image: new RegularShape({ 297 fill: new Fill({ color: 'rgba(0,0,0,0)' }), 298 points: 4, 299 radius: cfg.size, 300 rotation: (rotation * Math.PI) / 180 + Math.PI / 4, 301 }), 302 }), 303 ]; 304 } 305 : errorMarker, 306 }; 307 markerMakers.register(maker); 308 return maker.make; 309 } 310 311 // default to showing a circle 312 return errorMarker; 313} 314