Geomap: Add GeoJSON gazetteer (#40589)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
nikki-kiga 2021-10-19 21:05:55 -07:00 committed by GitHub
parent 43878ff05f
commit 2eb88e1d1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 5 deletions

View File

@ -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>
)}
</>

View File

@ -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',

View 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,
],
}
`);
});
});

View 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;
},
};
}

View File

@ -10,7 +10,7 @@ jest.mock('@grafana/runtime', () => ({
}),
}));
describe('Placename lookups', () => {
describe('Placename lookup from worldmap format', () => {
beforeEach(() => {
backendResults = { hello: 'world' };
});

View File

@ -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;

File diff suppressed because one or more lines are too long