mirror of https://github.com/grafana/grafana
Gazetteer: reactor so the source is a DataFrame (#43783)
parent
c68eefd398
commit
fd8baf5f7d
@ -0,0 +1,79 @@ |
||||
import { |
||||
Field, |
||||
FieldType, |
||||
FrameGeometrySource, |
||||
FrameGeometrySourceMode, |
||||
PanelOptionsEditorBuilder, |
||||
} from '@grafana/data'; |
||||
import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor'; |
||||
|
||||
export function addLocationFields<TOptions>( |
||||
prefix: string, |
||||
builder: PanelOptionsEditorBuilder<TOptions>, |
||||
source?: FrameGeometrySource |
||||
) { |
||||
builder.addRadio({ |
||||
path: `${prefix}.mode`, |
||||
name: 'Location', |
||||
description: '', |
||||
defaultValue: FrameGeometrySourceMode.Auto, |
||||
settings: { |
||||
options: [ |
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' }, |
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' }, |
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' }, |
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' }, |
||||
], |
||||
}, |
||||
}); |
||||
|
||||
switch (source?.mode) { |
||||
case FrameGeometrySourceMode.Coords: |
||||
builder |
||||
.addFieldNamePicker({ |
||||
path: `${prefix}.latitude`, |
||||
name: 'Latitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: `${prefix}.longitude`, |
||||
name: 'Longitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
}); |
||||
break; |
||||
|
||||
case FrameGeometrySourceMode.Geohash: |
||||
builder.addFieldNamePicker({ |
||||
path: `${prefix}.geohash`, |
||||
name: 'Geohash field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
}); |
||||
break; |
||||
|
||||
case FrameGeometrySourceMode.Lookup: |
||||
builder |
||||
.addFieldNamePicker({ |
||||
path: `${prefix}.lookup`, |
||||
name: 'Lookup field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'gazetteer', |
||||
path: `${prefix}.gazetteer`, |
||||
name: 'Gazetteer', |
||||
editor: GazetteerPathEditor, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
import { decodeGeohash } from './geohash'; |
||||
|
||||
describe('Read GeoHASH', () => { |
||||
it('simple decode', () => { |
||||
expect(decodeGeohash('9q94r')).toEqual([-122.01416015625, 36.97998046875]); |
||||
expect(decodeGeohash('dr5rs')).toEqual([-73.98193359375, 40.71533203125]); |
||||
}); |
||||
}); |
@ -0,0 +1,113 @@ |
||||
import { dataFrameToJSON, FieldType } from '@grafana/data'; |
||||
import { frameFromGeoJSON } from './geojson'; |
||||
|
||||
describe('Read GeoJSON', () => { |
||||
it('supports simple read', () => { |
||||
const frame = frameFromGeoJSON({ |
||||
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: { |
||||
number: 1.2, |
||||
hello: 'B', |
||||
mixed: 'first', |
||||
}, |
||||
}, |
||||
{ |
||||
type: 'Feature', |
||||
geometry: { |
||||
type: 'Point', |
||||
coordinates: [2, 2], |
||||
}, |
||||
properties: { |
||||
number: 2.3, |
||||
mixed: 2, |
||||
}, |
||||
}, |
||||
], |
||||
}); |
||||
const msg = dataFrameToJSON(frame); |
||||
expect(msg.schema).toMatchInlineSnapshot(` |
||||
Object { |
||||
"fields": Array [ |
||||
Object { |
||||
"config": Object {}, |
||||
"name": "id", |
||||
"type": "string", |
||||
}, |
||||
Object { |
||||
"config": Object {}, |
||||
"name": "geo", |
||||
"type": "geo", |
||||
}, |
||||
Object { |
||||
"config": Object {}, |
||||
"name": "hello", |
||||
"type": "string", |
||||
}, |
||||
Object { |
||||
"config": Object {}, |
||||
"name": "number", |
||||
"type": "number", |
||||
}, |
||||
Object { |
||||
"config": Object {}, |
||||
"name": "mixed", |
||||
"type": "string", |
||||
}, |
||||
], |
||||
"meta": undefined, |
||||
"name": undefined, |
||||
"refId": undefined, |
||||
} |
||||
`);
|
||||
|
||||
expect( |
||||
frame.fields.reduce((acc, v, idx, arr) => { |
||||
if (v.type !== FieldType.geo) { |
||||
acc[v.name] = v.values.toArray(); |
||||
} |
||||
return acc; |
||||
}, {} as any) |
||||
).toMatchInlineSnapshot(` |
||||
Object { |
||||
"hello": Array [ |
||||
"A", |
||||
"B", |
||||
null, |
||||
], |
||||
"id": Array [ |
||||
"A", |
||||
null, |
||||
null, |
||||
], |
||||
"mixed": Array [ |
||||
null, |
||||
"first", |
||||
"2", |
||||
], |
||||
"number": Array [ |
||||
null, |
||||
1.2, |
||||
2.3, |
||||
], |
||||
} |
||||
`);
|
||||
}); |
||||
}); |
@ -0,0 +1,125 @@ |
||||
import { ArrayVector, DataFrame, Field, FieldType, getFieldTypeFromValue } from '@grafana/data'; |
||||
import GeoJSON from 'ol/format/GeoJSON'; |
||||
import { Geometry } from 'ol/geom'; |
||||
|
||||
interface FieldInfo { |
||||
values: any[]; |
||||
types: Set<FieldType>; |
||||
count: number; |
||||
} |
||||
|
||||
// http://geojson.xyz/
|
||||
|
||||
export function frameFromGeoJSON(body: Document | Element | Object | string): DataFrame { |
||||
const data = new GeoJSON().readFeatures(body); |
||||
const length = data.length; |
||||
|
||||
const geo: Geometry[] = new Array(length).fill(null); |
||||
|
||||
const fieldOrder: string[] = []; |
||||
const lookup = new Map<string, FieldInfo>(); |
||||
const getField = (name: string) => { |
||||
let f = lookup.get(name); |
||||
if (!f) { |
||||
f = { |
||||
types: new Set<FieldType>(), |
||||
values: new Array(length).fill(null), |
||||
count: 0, |
||||
}; |
||||
fieldOrder.push(name); |
||||
lookup.set(name, f); |
||||
} |
||||
return f; |
||||
}; |
||||
const getBestName = (...names: string[]) => { |
||||
for (const k of names) { |
||||
if (!lookup.has(k)) { |
||||
return k; |
||||
} |
||||
} |
||||
return '___' + names[0]; |
||||
}; |
||||
|
||||
const idfield: FieldInfo = { |
||||
types: new Set<FieldType>(), |
||||
values: new Array(length).fill(null), |
||||
count: 0, |
||||
}; |
||||
for (let i = 0; i < length; i++) { |
||||
const feature = data[i]; |
||||
geo[i] = feature.getGeometry(); |
||||
|
||||
const id = feature.getId(); |
||||
if (id != null) { |
||||
idfield.values[i] = id; |
||||
idfield.types.add(getFieldTypeFromValue(id)); |
||||
idfield.count++; |
||||
} |
||||
|
||||
for (const key of feature.getKeys()) { |
||||
const val = feature.get(key); |
||||
if (key === 'geometry' && val === geo[i]) { |
||||
continue; |
||||
} |
||||
const field = getField(key); |
||||
field.values[i] = val; |
||||
field.types.add(getFieldTypeFromValue(val)); |
||||
field.count++; |
||||
} |
||||
} |
||||
|
||||
const fields: Field[] = []; |
||||
if (idfield.count > 0) { |
||||
const type = ensureSingleType(idfield); |
||||
fields.push({ |
||||
name: getBestName('id', '_id', '__id'), |
||||
type, |
||||
values: new ArrayVector(idfield.values), |
||||
config: {}, |
||||
}); |
||||
} |
||||
|
||||
// Add a geometry field
|
||||
fields.push({ |
||||
name: getBestName('geo', 'geometry'), |
||||
type: FieldType.geo, |
||||
values: new ArrayVector(geo), |
||||
config: {}, |
||||
}); |
||||
|
||||
for (const name of fieldOrder) { |
||||
const info = lookup.get(name); |
||||
if (!info) { |
||||
continue; |
||||
} |
||||
const type = ensureSingleType(info); |
||||
fields.push({ |
||||
name, |
||||
type, |
||||
values: new ArrayVector(info.values), |
||||
config: {}, |
||||
}); |
||||
} |
||||
|
||||
// Simple frame
|
||||
return { |
||||
fields, |
||||
length, |
||||
}; |
||||
} |
||||
|
||||
function ensureSingleType(info: FieldInfo): FieldType { |
||||
if (info.count < 1) { |
||||
return FieldType.other; |
||||
} |
||||
if (info.types.size > 1) { |
||||
info.values = info.values.map((v) => { |
||||
if (v != null) { |
||||
return `${v}`; |
||||
} |
||||
return v; |
||||
}); |
||||
return FieldType.string; |
||||
} |
||||
return info.types.values().next().value; |
||||
} |
@ -0,0 +1,56 @@ |
||||
import { ArrayVector, Field, FieldConfig, FieldType } from '@grafana/data'; |
||||
import { Geometry, Point } from 'ol/geom'; |
||||
import { fromLonLat } from 'ol/proj'; |
||||
import { Gazetteer } from '../gazetteer/gazetteer'; |
||||
import { decodeGeohash } from './geohash'; |
||||
|
||||
export function pointFieldFromGeohash(geohash: Field<string>): Field<Point> { |
||||
return { |
||||
name: 'point', |
||||
type: FieldType.geo, |
||||
values: new ArrayVector<any>( |
||||
geohash.values.toArray().map((v) => { |
||||
const coords = decodeGeohash(v); |
||||
if (coords) { |
||||
return new Point(fromLonLat(coords)); |
||||
} |
||||
return undefined; |
||||
}) |
||||
), |
||||
config: hiddenTooltipField, |
||||
}; |
||||
} |
||||
|
||||
export function pointFieldFromLonLat(lon: Field, lat: Field): Field<Point> { |
||||
const buffer = new Array<Point>(lon.values.length); |
||||
for (let i = 0; i < lon.values.length; i++) { |
||||
buffer[i] = new Point(fromLonLat([lon.values.get(i), lat.values.get(i)])); |
||||
} |
||||
|
||||
return { |
||||
name: 'point', |
||||
type: FieldType.geo, |
||||
values: new ArrayVector(buffer), |
||||
config: hiddenTooltipField, |
||||
}; |
||||
} |
||||
|
||||
export function getGeoFieldFromGazetteer(gaz: Gazetteer, field: Field<string>): Field<Geometry | undefined> { |
||||
const count = field.values.length; |
||||
const geo = new Array<Geometry | undefined>(count); |
||||
for (let i = 0; i < count; i++) { |
||||
geo[i] = gaz.find(field.values.get(i))?.geometry(); |
||||
} |
||||
return { |
||||
name: 'Geometry', |
||||
type: FieldType.geo, |
||||
values: new ArrayVector(geo), |
||||
config: hiddenTooltipField, |
||||
}; |
||||
} |
||||
|
||||
const hiddenTooltipField: FieldConfig = Object.freeze({ |
||||
custom: { |
||||
hideFrom: { tooltip: true }, |
||||
}, |
||||
}); |
@ -0,0 +1,201 @@ |
||||
import { DataFrame, Field, FieldType, KeyValue, toDataFrame } from '@grafana/data'; |
||||
import { getBackendSrv } from '@grafana/runtime'; |
||||
import { loadWorldmapPoints } from './worldmap'; |
||||
import { Geometry, Point } from 'ol/geom'; |
||||
import { frameFromGeoJSON } from '../format/geojson'; |
||||
import { pointFieldFromLonLat, pointFieldFromGeohash } from '../format/utils'; |
||||
import { getCenter } from 'ol/extent'; |
||||
|
||||
export interface PlacenameInfo { |
||||
point: () => Point | undefined; // lon, lat (WGS84)
|
||||
geometry: () => Geometry | undefined; |
||||
frame?: DataFrame; |
||||
index?: number; |
||||
} |
||||
|
||||
export interface Gazetteer { |
||||
path: string; |
||||
error?: string; |
||||
find: (key: string) => PlacenameInfo | undefined; |
||||
examples: (count: number) => string[]; |
||||
frame?: () => DataFrame; |
||||
count?: number; |
||||
} |
||||
|
||||
// Without knowing the datatype pick a good lookup function
|
||||
export function loadGazetteer(path: string, data: any): Gazetteer { |
||||
// try loading geojson
|
||||
let frame: DataFrame | undefined = undefined; |
||||
|
||||
if (Array.isArray(data)) { |
||||
const first = data[0] as any; |
||||
// Check for legacy worldmap syntax
|
||||
if (first.latitude && first.longitude && (first.key || first.keys)) { |
||||
return loadWorldmapPoints(path, data); |
||||
} |
||||
} else { |
||||
if (Array.isArray(data?.features) && data?.type === 'FeatureCollection') { |
||||
frame = frameFromGeoJSON(data); |
||||
} |
||||
} |
||||
|
||||
if (!frame) { |
||||
try { |
||||
frame = toDataFrame(data); |
||||
} catch (ex) { |
||||
return { |
||||
path, |
||||
error: `${ex}`, |
||||
find: (k) => undefined, |
||||
examples: (v) => [], |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return frameAsGazetter(frame, { path }); |
||||
} |
||||
|
||||
export function frameAsGazetter(frame: DataFrame, opts: { path: string; keys?: string[] }): Gazetteer { |
||||
const keys: Field[] = []; |
||||
let geo: Field<Geometry> | undefined = undefined; |
||||
let lat: Field | undefined = undefined; |
||||
let lng: Field | undefined = undefined; |
||||
let geohash: Field | undefined = undefined; |
||||
let firstString: Field | undefined = undefined; |
||||
for (const f of frame.fields) { |
||||
if (f.type === FieldType.geo) { |
||||
geo = f; |
||||
} |
||||
if (!firstString && f.type === FieldType.string) { |
||||
firstString = f; |
||||
} |
||||
if (f.name) { |
||||
if (opts.keys && opts.keys.includes(f.name)) { |
||||
keys.push(f); |
||||
} |
||||
|
||||
const name = f.name.toUpperCase(); |
||||
switch (name) { |
||||
case 'LAT': |
||||
case 'LATITUTE': |
||||
lat = f; |
||||
break; |
||||
|
||||
case 'LON': |
||||
case 'LNG': |
||||
case 'LONG': |
||||
case 'LONGITUE': |
||||
lng = f; |
||||
break; |
||||
|
||||
case 'GEOHASH': |
||||
geohash = f; |
||||
break; |
||||
|
||||
case 'ID': |
||||
case 'UID': |
||||
case 'KEY': |
||||
case 'CODE': |
||||
if (!opts.keys) { |
||||
keys.push(f); |
||||
} |
||||
break; |
||||
|
||||
default: { |
||||
if (!opts.keys) { |
||||
if (name.endsWith('_ID') || name.endsWith('_CODE')) { |
||||
keys.push(f); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Use the first string field
|
||||
if (!keys.length && firstString) { |
||||
keys.push(firstString); |
||||
} |
||||
|
||||
let isPoint = false; |
||||
|
||||
// Create a geo field from lat+lng
|
||||
if (!geo) { |
||||
if (geohash) { |
||||
geo = pointFieldFromGeohash(geohash); |
||||
isPoint = true; |
||||
} else if (lat && lng) { |
||||
geo = pointFieldFromLonLat(lng, lat); |
||||
isPoint = true; |
||||
} |
||||
} else { |
||||
isPoint = geo.values.get(0).getType() === 'Point'; |
||||
} |
||||
|
||||
const lookup = new Map<string, number>(); |
||||
keys.forEach((f) => { |
||||
f.values.toArray().forEach((k, idx) => { |
||||
const str = `${k}`; |
||||
lookup.set(str.toUpperCase(), idx); |
||||
lookup.set(str, idx); |
||||
}); |
||||
}); |
||||
|
||||
return { |
||||
path: opts.path, |
||||
find: (k) => { |
||||
const index = lookup.get(k); |
||||
if (index != null) { |
||||
const g = geo?.values.get(index); |
||||
return { |
||||
frame, |
||||
index, |
||||
point: () => { |
||||
if (!g || isPoint) { |
||||
return g as Point; |
||||
} |
||||
return new Point(getCenter(g.getExtent())); |
||||
}, |
||||
geometry: () => g, |
||||
}; |
||||
} |
||||
return undefined; |
||||
}, |
||||
examples: (v) => [], |
||||
frame: () => frame, |
||||
count: frame.length, |
||||
}; |
||||
} |
||||
|
||||
const registry: KeyValue<Gazetteer> = {}; |
||||
|
||||
export const COUNTRIES_GAZETTEER_PATH = 'public/gazetteer/countries.json'; |
||||
|
||||
/** |
||||
* Given a path to a file return a cached lookup function |
||||
*/ |
||||
export async function getGazetteer(path?: string): Promise<Gazetteer> { |
||||
// When not specified, use the default path
|
||||
if (!path) { |
||||
path = COUNTRIES_GAZETTEER_PATH; |
||||
} |
||||
|
||||
let lookup = registry[path]; |
||||
if (!lookup) { |
||||
try { |
||||
// block the async function
|
||||
const data = await getBackendSrv().get(path!); |
||||
lookup = loadGazetteer(path, data); |
||||
} catch (err) { |
||||
console.warn('Error loading placename lookup', path, err); |
||||
lookup = { |
||||
path, |
||||
error: 'Error loading URL', |
||||
find: (k) => undefined, |
||||
examples: (v) => [], |
||||
}; |
||||
} |
||||
registry[path] = lookup; |
||||
} |
||||
return lookup; |
||||
} |
@ -0,0 +1,35 @@ |
||||
import { DataFrame } from '@grafana/data'; |
||||
import { Feature } from 'ol'; |
||||
import { Geometry } from 'ol/geom'; |
||||
import VectorSource from 'ol/source/Vector'; |
||||
import { getGeometryField, LocationFieldMatchers } from './location'; |
||||
|
||||
export interface FrameVectorSourceOptions {} |
||||
|
||||
export class FrameVectorSource extends VectorSource<Geometry> { |
||||
constructor(private location: LocationFieldMatchers) { |
||||
super({}); |
||||
} |
||||
|
||||
update(frame: DataFrame) { |
||||
this.clear(true); |
||||
const info = getGeometryField(frame, this.location); |
||||
if (!info.field) { |
||||
this.changed(); |
||||
return; |
||||
} |
||||
|
||||
for (let i = 0; i < frame.length; i++) { |
||||
this.addFeatureInternal( |
||||
new Feature({ |
||||
frame, |
||||
rowIndex: i, |
||||
geometry: info.field.values.get(i), |
||||
}) |
||||
); |
||||
} |
||||
|
||||
// only call this at the end
|
||||
this.changed(); |
||||
} |
||||
} |
@ -1,76 +0,0 @@ |
||||
import { KeyValue } from '@grafana/data'; |
||||
import { getBackendSrv } from '@grafana/runtime'; |
||||
import { loadWorldmapPoints } from './worldmap'; |
||||
import { loadFromGeoJSON } from './geojson'; |
||||
|
||||
// http://geojson.xyz/
|
||||
|
||||
export interface PlacenameInfo { |
||||
coords: [number, number]; // lon, lat (WGS84)
|
||||
props?: Record<string, any>; |
||||
} |
||||
|
||||
export interface Gazetteer { |
||||
path: string; |
||||
error?: string; |
||||
find: (key: string) => PlacenameInfo | undefined; |
||||
count?: number; |
||||
examples: (count: number) => string[]; |
||||
} |
||||
|
||||
// Without knowing the datatype pick a good lookup function
|
||||
export function loadGazetteer(path: string, data: any): Gazetteer { |
||||
// Check for legacy worldmap syntax
|
||||
if (Array.isArray(data)) { |
||||
const first = data[0] as any; |
||||
if (first.latitude && first.longitude && (first.key || first.keys)) { |
||||
return loadWorldmapPoints(path, data); |
||||
} |
||||
} |
||||
|
||||
// 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', |
||||
find: (k) => undefined, |
||||
examples: (v) => [], |
||||
}; |
||||
} |
||||
|
||||
const registry: KeyValue<Gazetteer> = {}; |
||||
|
||||
export const COUNTRIES_GAZETTEER_PATH = 'public/gazetteer/countries.json'; |
||||
|
||||
/** |
||||
* Given a path to a file return a cached lookup function |
||||
*/ |
||||
export async function getGazetteer(path?: string): Promise<Gazetteer> { |
||||
// When not specified, use the default path
|
||||
if (!path) { |
||||
path = COUNTRIES_GAZETTEER_PATH; |
||||
} |
||||
|
||||
let lookup = registry[path]; |
||||
if (!lookup) { |
||||
try { |
||||
// block the async function
|
||||
const data = await getBackendSrv().get(path!); |
||||
lookup = loadGazetteer(path, data); |
||||
} catch (err) { |
||||
console.warn('Error loading placename lookup', path, err); |
||||
lookup = { |
||||
path, |
||||
error: 'Error loading URL', |
||||
find: (k) => undefined, |
||||
examples: (v) => [], |
||||
}; |
||||
} |
||||
registry[path] = lookup; |
||||
} |
||||
return lookup; |
||||
} |
@ -1,75 +0,0 @@ |
||||
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; |
||||
}, |
||||
}; |
||||
} |
Loading…
Reference in new issue