gazetteer.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { getCenter } from 'ol/extent';
  2. import { Geometry, Point } from 'ol/geom';
  3. import { DataFrame, Field, FieldType, KeyValue, toDataFrame } from '@grafana/data';
  4. import { getBackendSrv } from '@grafana/runtime';
  5. import { frameFromGeoJSON } from '../format/geojson';
  6. import { pointFieldFromLonLat, pointFieldFromGeohash } from '../format/utils';
  7. import { loadWorldmapPoints } from './worldmap';
  8. export interface PlacenameInfo {
  9. point: () => Point | undefined; // lon, lat (WGS84)
  10. geometry: () => Geometry | undefined;
  11. frame?: DataFrame;
  12. index?: number;
  13. }
  14. export interface Gazetteer {
  15. path: string;
  16. error?: string;
  17. find: (key: string) => PlacenameInfo | undefined;
  18. examples: (count: number) => string[];
  19. frame?: () => DataFrame;
  20. count?: number;
  21. }
  22. // Without knowing the datatype pick a good lookup function
  23. export function loadGazetteer(path: string, data: any): Gazetteer {
  24. // try loading geojson
  25. let frame: DataFrame | undefined = undefined;
  26. if (Array.isArray(data)) {
  27. const first = data[0] as any;
  28. // Check for legacy worldmap syntax
  29. if (first.latitude && first.longitude && (first.key || first.keys)) {
  30. return loadWorldmapPoints(path, data);
  31. }
  32. } else {
  33. if (Array.isArray(data?.features) && data?.type === 'FeatureCollection') {
  34. frame = frameFromGeoJSON(data);
  35. }
  36. }
  37. if (!frame) {
  38. try {
  39. frame = toDataFrame(data);
  40. } catch (ex) {
  41. return {
  42. path,
  43. error: `${ex}`,
  44. find: (k) => undefined,
  45. examples: (v) => [],
  46. };
  47. }
  48. }
  49. return frameAsGazetter(frame, { path });
  50. }
  51. export function frameAsGazetter(frame: DataFrame, opts: { path: string; keys?: string[] }): Gazetteer {
  52. const keys: Field[] = [];
  53. let geo: Field<Geometry> | undefined = undefined;
  54. let lat: Field | undefined = undefined;
  55. let lng: Field | undefined = undefined;
  56. let geohash: Field | undefined = undefined;
  57. let firstString: Field | undefined = undefined;
  58. for (const f of frame.fields) {
  59. if (f.type === FieldType.geo) {
  60. geo = f;
  61. }
  62. if (!firstString && f.type === FieldType.string) {
  63. firstString = f;
  64. }
  65. if (f.name) {
  66. if (opts.keys && opts.keys.includes(f.name)) {
  67. keys.push(f);
  68. }
  69. const name = f.name.toUpperCase();
  70. switch (name) {
  71. case 'LAT':
  72. case 'LATITUTE':
  73. lat = f;
  74. break;
  75. case 'LON':
  76. case 'LNG':
  77. case 'LONG':
  78. case 'LONGITUE':
  79. lng = f;
  80. break;
  81. case 'GEOHASH':
  82. geohash = f;
  83. break;
  84. case 'ID':
  85. case 'UID':
  86. case 'KEY':
  87. case 'CODE':
  88. if (!opts.keys) {
  89. keys.push(f);
  90. }
  91. break;
  92. default: {
  93. if (!opts.keys) {
  94. if (name.endsWith('_ID') || name.endsWith('_CODE')) {
  95. keys.push(f);
  96. }
  97. }
  98. }
  99. }
  100. }
  101. }
  102. // Use the first string field
  103. if (!keys.length && firstString) {
  104. keys.push(firstString);
  105. }
  106. let isPoint = false;
  107. // Create a geo field from lat+lng
  108. if (!geo) {
  109. if (geohash) {
  110. geo = pointFieldFromGeohash(geohash);
  111. isPoint = true;
  112. } else if (lat && lng) {
  113. geo = pointFieldFromLonLat(lng, lat);
  114. isPoint = true;
  115. }
  116. } else {
  117. isPoint = geo.values.get(0).getType() === 'Point';
  118. }
  119. const lookup = new Map<string, number>();
  120. keys.forEach((f) => {
  121. f.values.toArray().forEach((k, idx) => {
  122. const str = `${k}`;
  123. lookup.set(str.toUpperCase(), idx);
  124. lookup.set(str, idx);
  125. });
  126. });
  127. return {
  128. path: opts.path,
  129. find: (k) => {
  130. const index = lookup.get(k);
  131. if (index != null) {
  132. const g = geo?.values.get(index);
  133. return {
  134. frame,
  135. index,
  136. point: () => {
  137. if (!g || isPoint) {
  138. return g as Point;
  139. }
  140. return new Point(getCenter(g.getExtent()));
  141. },
  142. geometry: () => g,
  143. };
  144. }
  145. return undefined;
  146. },
  147. examples: (v) => {
  148. const ex: string[] = [];
  149. for (let k of lookup.keys()) {
  150. ex.push(k);
  151. if (ex.length > v) {
  152. break;
  153. }
  154. }
  155. return ex;
  156. },
  157. frame: () => frame,
  158. count: frame.length,
  159. };
  160. }
  161. const registry: KeyValue<Gazetteer> = {};
  162. export const COUNTRIES_GAZETTEER_PATH = 'public/gazetteer/countries.json';
  163. /**
  164. * Given a path to a file return a cached lookup function
  165. */
  166. export async function getGazetteer(path?: string): Promise<Gazetteer> {
  167. // When not specified, use the default path
  168. if (!path) {
  169. path = COUNTRIES_GAZETTEER_PATH;
  170. }
  171. let lookup = registry[path];
  172. if (!lookup) {
  173. try {
  174. // block the async function
  175. const data = await getBackendSrv().get(path!);
  176. lookup = loadGazetteer(path, data);
  177. } catch (err) {
  178. console.warn('Error loading placename lookup', path, err);
  179. lookup = {
  180. path,
  181. error: 'Error loading URL',
  182. find: (k) => undefined,
  183. examples: (v) => [],
  184. };
  185. }
  186. registry[path] = lookup;
  187. }
  188. return lookup;
  189. }