mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
Geomap: Add GeoJSON gazetteer (#40589)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
43878ff05f
commit
2eb88e1d1b
@ -7,7 +7,7 @@ import { css } from '@emotion/css';
|
||||
const paths: Array<SelectableValue<string>> = [
|
||||
{
|
||||
label: 'Countries',
|
||||
description: 'Lookup countries by name, two letter code, or three leter code',
|
||||
description: 'Lookup countries by name, two letter code, or three letter code',
|
||||
value: COUNTRIES_GAZETTEER_PATH,
|
||||
},
|
||||
{
|
||||
@ -15,6 +15,11 @@ const paths: Array<SelectableValue<string>> = [
|
||||
description: 'Lookup states by name or 2 ',
|
||||
value: 'public/gazetteer/usa-states.json',
|
||||
},
|
||||
{
|
||||
label: 'Airports',
|
||||
description: 'Lookup airports by id or code',
|
||||
value: 'public/gazetteer/airports.geojson',
|
||||
},
|
||||
];
|
||||
|
||||
export const GazetteerPathEditor: FC<StandardEditorProps<string, any, any>> = ({ value, onChange, context }) => {
|
||||
@ -60,8 +65,8 @@ export const GazetteerPathEditor: FC<StandardEditorProps<string, any, any>> = ({
|
||||
<b>({gaz.count})</b>
|
||||
{gaz.examples(10).map((k) => (
|
||||
<span key={k}>{k},</span>
|
||||
))}{' '}
|
||||
&ellipsis;
|
||||
))}
|
||||
{gaz.count > 10 && ' ...'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { KeyValue } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { loadWorldmapPoints } from './worldmap';
|
||||
import { loadFromGeoJSON } from './geojson';
|
||||
|
||||
// http://geojson.xyz/
|
||||
|
||||
@ -27,6 +28,12 @@ export function loadGazetteer(path: string, data: any): Gazetteer {
|
||||
}
|
||||
}
|
||||
|
||||
// try loading geojson
|
||||
const features = data?.features;
|
||||
if (Array.isArray(features) && data?.type === 'FeatureCollection') {
|
||||
return loadFromGeoJSON(path, data);
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
error: 'Unable to parse locations',
|
||||
|
96
public/app/plugins/panel/geomap/gazetteer/geojson.test.ts
Normal file
96
public/app/plugins/panel/geomap/gazetteer/geojson.test.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { getGazetteer } from './gazetteer';
|
||||
|
||||
let backendResults: any = { hello: 'world' };
|
||||
|
||||
const geojsonObject = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
id: 'A',
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [0, 0],
|
||||
},
|
||||
properties: {
|
||||
hello: 'A',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [1, 1],
|
||||
},
|
||||
properties: {
|
||||
some_code: 'B',
|
||||
hello: 'B',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [2, 2],
|
||||
},
|
||||
properties: {
|
||||
an_id: 'C',
|
||||
hello: 'C',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
getBackendSrv: () => ({
|
||||
get: jest.fn().mockResolvedValue(backendResults),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Placename lookup from geojson format', () => {
|
||||
beforeEach(() => {
|
||||
backendResults = { hello: 'world' };
|
||||
});
|
||||
|
||||
it('can lookup by id', async () => {
|
||||
backendResults = geojsonObject;
|
||||
const gaz = await getGazetteer('local');
|
||||
expect(gaz.error).toBeUndefined();
|
||||
expect(gaz.find('A')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"coords": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('can look up by a code', async () => {
|
||||
backendResults = geojsonObject;
|
||||
const gaz = await getGazetteer('airports');
|
||||
expect(gaz.error).toBeUndefined();
|
||||
expect(gaz.find('B')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"coords": Array [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can look up by an id property', async () => {
|
||||
backendResults = geojsonObject;
|
||||
const gaz = await getGazetteer('airports');
|
||||
expect(gaz.error).toBeUndefined();
|
||||
expect(gaz.find('C')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"coords": Array [
|
||||
2,
|
||||
2,
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
75
public/app/plugins/panel/geomap/gazetteer/geojson.ts
Normal file
75
public/app/plugins/panel/geomap/gazetteer/geojson.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
import { PlacenameInfo, Gazetteer } from './gazetteer';
|
||||
|
||||
export interface GeoJSONPoint {
|
||||
key?: string;
|
||||
keys?: string[]; // new in grafana 8.1+
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function loadFromGeoJSON(path: string, body: any): Gazetteer {
|
||||
const data = new GeoJSON().readFeatures(body);
|
||||
let count = 0;
|
||||
const values = new Map<string, PlacenameInfo>();
|
||||
for (const f of data) {
|
||||
const coords = f.getGeometry().getFlatCoordinates(); //for now point, eventually geometry
|
||||
const info: PlacenameInfo = {
|
||||
coords: coords,
|
||||
};
|
||||
const id = f.getId();
|
||||
if (id) {
|
||||
if (typeof id === 'number') {
|
||||
values.set(id.toString(), info);
|
||||
} else {
|
||||
values.set(id, info);
|
||||
values.set(id.toUpperCase(), info);
|
||||
}
|
||||
}
|
||||
const properties = f.getProperties();
|
||||
if (properties) {
|
||||
for (const k of Object.keys(properties)) {
|
||||
if (k.includes('_code') || k.includes('_id')) {
|
||||
const value = properties[k];
|
||||
if (value) {
|
||||
if (typeof value === 'number') {
|
||||
values.set(value.toString(), info);
|
||||
} else {
|
||||
values.set(value, info);
|
||||
values.set(value.toUpperCase(), info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
find: (k) => {
|
||||
let v = values.get(k);
|
||||
if (!v && typeof k === 'string') {
|
||||
v = values.get(k.toUpperCase());
|
||||
}
|
||||
return v;
|
||||
},
|
||||
count,
|
||||
examples: (count) => {
|
||||
const first: string[] = [];
|
||||
if (values.size < 1) {
|
||||
first.push('no values found');
|
||||
} else {
|
||||
for (const key of values.keys()) {
|
||||
first.push(key);
|
||||
if (first.length >= count) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return first;
|
||||
},
|
||||
};
|
||||
}
|
@ -10,7 +10,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Placename lookups', () => {
|
||||
describe('Placename lookup from worldmap format', () => {
|
||||
beforeEach(() => {
|
||||
backendResults = { hello: 'world' };
|
||||
});
|
@ -37,7 +37,7 @@ export function loadWorldmapPoints(path: string, data: WorldmapPoint[]): Gazette
|
||||
path,
|
||||
find: (k) => {
|
||||
let v = values.get(k);
|
||||
if (!v) {
|
||||
if (!v && typeof k === 'string') {
|
||||
v = values.get(k.toUpperCase());
|
||||
}
|
||||
return v;
|
||||
|
1
public/gazetteer/airports.geojson
Normal file
1
public/gazetteer/airports.geojson
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user