[Maps] Custom raster source example plugin (#136761)

This commit is contained in:
Nick Peihl 2022-07-26 17:41:45 -04:00 committed by GitHub
parent 8b0d873e1e
commit 69dc7e2b77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 457 additions and 10 deletions

View file

@ -12,6 +12,7 @@ import type {
Source,
GeoJSONSource,
VectorTileSource,
RasterTileSource,
StyleSpecification,
MapEvent,
MapOptions,
@ -48,6 +49,7 @@ export type {
Source,
GeoJSONSource,
VectorTileSource,
RasterTileSource,
MapEvent,
MapOptions,
MapMouseEvent,

View file

@ -279,6 +279,8 @@
"@kbn/testing-embedded-lens-plugin/*": ["x-pack/examples/testing_embedded_lens/*"],
"@kbn/third-party-lens-navigation-prompt-plugin": ["x-pack/examples/third_party_lens_navigation_prompt"],
"@kbn/third-party-lens-navigation-prompt-plugin/*": ["x-pack/examples/third_party_lens_navigation_prompt/*"],
"@kbn/maps-custom-raster-source-plugin": ["x-pack/examples/third_party_maps_source_example"],
"@kbn/maps-custom-raster-source-plugin/*": ["x-pack/examples/third_party_maps_source_example/*"],
"@kbn/third-party-vis-lens-example-plugin": ["x-pack/examples/third_party_vis_lens_example"],
"@kbn/third-party-vis-lens-example-plugin/*": ["x-pack/examples/third_party_vis_lens_example/*"],
"@kbn/triggers-actions-ui-example-plugin": ["x-pack/examples/triggers_actions_ui_example"],

View file

@ -0,0 +1,17 @@
# Third party maps source example plugin
An example plugin for a custom raster tile source in Maps.
This example plugin uses a [time-enabled radar imagery service from the U.S. National Weather Service](https://idpgis.ncep.noaa.gov/arcgis/rest/services/radar/radar_base_reflectivity_time/ImageServer). The service URL contains a `{time}` template field that is populated as [Unix time](https://en.wikipedia.org/wiki/Unix_time) from the Kibana time picker. The time slider in Maps can also be used to animate the service.
## Demo
1. Open a new Map and modify the time picker to the "Last 2 hours".
2. Click "Add layer" and choose "Weather" to add the layer to the map.
3. You should see current precipitation models over the U.S.
4. Use the timeslider to animate the radar model.
---
## Development
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const PLUGIN_ID = 'mapsCustomRasterSource';
export const PLUGIN_NAME = 'Third party maps source example';

View file

@ -0,0 +1,15 @@
{
"id": "mapsCustomRasterSource",
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["third_party_maps_source_example"],
"owner": {
"name": "kibana-gis",
"githubTeam": "kibana-gis"
},
"description": "An example plugin for creating a custom raster source for Elastic Maps",
"server": false,
"ui": true,
"requiredPlugins": ["navigation", "data", "maps", "developerExamples", "kibanaReact"],
"optionalPlugins": ["kibanaUtils", "kibanaReact"]
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { Component } from 'react';
import { EuiCallOut, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import { RenderWizardArguments } from '@kbn/maps-plugin/public';
import { LayerDescriptor, LAYER_TYPE } from '@kbn/maps-plugin/common';
import { CustomRasterSource } from './custom_raster_source';
export class CustomRasterEditor extends Component<RenderWizardArguments> {
componentDidMount() {
const customRasterLayerDescriptor: LayerDescriptor = {
id: htmlIdGenerator()(),
type: LAYER_TYPE.RASTER_TILE,
sourceDescriptor: CustomRasterSource.createDescriptor(),
style: {
type: 'RASTER',
},
alpha: 1,
};
this.props.previewLayers([customRasterLayerDescriptor]);
}
render() {
return (
<EuiPanel>
<EuiCallOut title="NOAA Weather">
<p>
Displays NOAA weather data. Kibana time is passed to request so weather data is
displayed for selected time range.
</p>
</EuiCallOut>
</EuiPanel>
);
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { LAYER_WIZARD_CATEGORY } from '@kbn/maps-plugin/common';
import type { LayerWizard, RenderWizardArguments } from '@kbn/maps-plugin/public';
import { PLUGIN_ID } from '../../common';
import { CustomRasterEditor } from './custom_raster_editor';
export const customRasterLayerWizard: LayerWizard = {
id: PLUGIN_ID,
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
title: 'Weather',
description: 'Weather data provided by NOAA',
icon: '',
order: 100,
renderWizard: (renderWizardArguments: RenderWizardArguments) => {
return <CustomRasterEditor {...renderWizardArguments} />;
},
};

View file

@ -0,0 +1,171 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ReactElement } from 'react';
import { calculateBounds } from '@kbn/data-plugin/common';
import { FieldFormatter, MIN_ZOOM, MAX_ZOOM } from '@kbn/maps-plugin/common';
import type {
AbstractSourceDescriptor,
Attribution,
DataFilters,
DataRequestMeta,
Timeslice,
} from '@kbn/maps-plugin/common/descriptor_types';
import type {
IField,
ImmutableSourceProperty,
ITMSSource,
SourceEditorArgs,
} from '@kbn/maps-plugin/public';
type CustomRasterSourceDescriptor = AbstractSourceDescriptor;
export class CustomRasterSource implements ITMSSource {
static type = 'CUSTOM_RASTER';
readonly _descriptor: CustomRasterSourceDescriptor;
static createDescriptor(): CustomRasterSourceDescriptor {
return {
type: CustomRasterSource.type,
};
}
constructor(sourceDescriptor: CustomRasterSourceDescriptor) {
this._descriptor = sourceDescriptor;
}
cloneDescriptor(): CustomRasterSourceDescriptor {
return {
type: this._descriptor.type,
};
}
async supportsFitToBounds(): Promise<boolean> {
return false;
}
/**
* return list of immutable source properties.
* Immutable source properties are properties that can not be edited by the user.
*/
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
return [];
}
getType(): string {
return this._descriptor.type;
}
async getDisplayName(): Promise<string> {
return '';
}
getAttributionProvider(): (() => Promise<Attribution[]>) | null {
return null;
}
isFieldAware(): boolean {
return false;
}
isGeoGridPrecisionAware(): boolean {
return false;
}
isQueryAware(): boolean {
return false;
}
getFieldNames(): string[] {
return [];
}
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null {
return null;
}
getApplyGlobalQuery(): boolean {
return false;
}
getApplyGlobalTime(): boolean {
return true;
}
getApplyForceRefresh(): boolean {
return false;
}
getIndexPatternIds(): string[] {
return [];
}
getQueryableIndexPatternIds(): string[] {
return [];
}
getGeoGridPrecision(zoom: number): number {
return 0;
}
isESSource(): boolean {
return false;
}
// Returns function used to format value
async createFieldFormatter(field: IField): Promise<FieldFormatter | null> {
return null;
}
async getValueSuggestions(field: IField, query: string): Promise<string[]> {
return [];
}
async isTimeAware(): Promise<boolean> {
return true;
}
isFilterByMapBounds(): boolean {
return false;
}
getMinZoom(): number {
return MIN_ZOOM;
}
getMaxZoom(): number {
return MAX_ZOOM;
}
async getLicensedFeatures(): Promise<[]> {
return [];
}
getUpdateDueToTimeslice(prevMeta: DataRequestMeta, timeslice?: Timeslice): boolean {
return true;
}
async getUrlTemplate(dataFilters: DataFilters): Promise<string> {
const defaultUrl =
'https://new.nowcoast.noaa.gov/arcgis/rest/services/nowcoast/radar_meteo_imagery_nexrad_time/MapServer/export?dpi=96&transparent=true&format=png32&time={time}&layers=show%3A3&bbox=-{bbox-epsg-3857}&bboxSR=3857&imageSR=3857&size=256%2C256&f=image';
const { timeslice, timeFilters } = dataFilters;
let timestamp;
if (timeslice) {
// Use the value from the timeslider
timestamp = new Date(timeslice.to).getTime();
} else {
const { max } = calculateBounds(timeFilters);
timestamp = max ? max.valueOf() : Date.now();
}
// Replace the '{time}' template value in the URL with the Unix timestamp
return defaultUrl.replace('{time}', timestamp.toString());
}
}

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import './index.scss';
import { MapsCustomRasterSourcePlugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new MapsCustomRasterSourcePlugin();
}
export type { MapsCustomRasterSourcePluginSetup, MapsCustomRasterSourcePluginStart } from './types';

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreSetup, Plugin } from '@kbn/core/public';
import { MapsCustomRasterSourcePluginSetup, MapsCustomRasterSourcePluginStart } from './types';
import { CustomRasterSource } from './classes/custom_raster_source';
import { customRasterLayerWizard } from './classes/custom_raster_layer_wizard';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import image from './third_party_maps_source_example.png';
export class MapsCustomRasterSourcePlugin
implements
Plugin<void, void, MapsCustomRasterSourcePluginSetup, MapsCustomRasterSourcePluginStart>
{
public setup(
core: CoreSetup<MapsCustomRasterSourcePluginStart>,
{ developerExamples, maps: mapsSetup }: MapsCustomRasterSourcePluginSetup
) {
// Register the Custom raster layer wizard with the Maps application
mapsSetup.registerSource({
type: CustomRasterSource.type,
ConstructorFunction: CustomRasterSource,
});
mapsSetup.registerLayerWizard(customRasterLayerWizard);
// Register an application into the side navigation menu
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
mount: ({ history }) => {
(async () => {
const [coreStart] = await core.getStartServices();
// if it's a regular navigation, open a new map
if (history.action === 'PUSH') {
coreStart.application.navigateToApp('maps', { path: 'map' });
} else {
coreStart.application.navigateToApp('developerExamples');
}
})();
return () => {};
},
});
developerExamples.register({
appId: PLUGIN_ID,
title: PLUGIN_NAME,
description: 'Example of using a third-party custom source with Elastic Maps',
image,
links: [
{
label: 'README',
href: 'https://github.com/elastic/kibana/tree/main/x-pack/examples/third_party_maps_source_example',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
}
public start() {}
public stop() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import { MapsPluginSetup, MapsPluginStart } from '@kbn/maps-plugin/public/plugin';
export interface MapsCustomRasterSourcePluginSetup {
developerExamples: DeveloperExamplesSetup;
maps: MapsPluginSetup;
}
export interface MapsCustomRasterSourcePluginStart {
maps: MapsPluginStart;
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
},
"include": [
"public/**/*.ts",
"public/**/*.tsx",
"common/**/*.ts",
"../../../typings/**/*",
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../plugins/maps/tsconfig.json"},
{ "path": "../../../examples/developer_examples/tsconfig.json" },
]
}

View file

@ -5,14 +5,19 @@
* 2.0.
*/
import type { Map as MbMap } from '@kbn/mapbox-gl';
import type { Map as MbMap, RasterTileSource } from '@kbn/mapbox-gl';
import _ from 'lodash';
import { AbstractLayer } from '../layer';
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants';
import { LayerDescriptor } from '../../../../common/descriptor_types';
import { LayerDescriptor, Timeslice } from '../../../../common/descriptor_types';
import { TileStyle } from '../../styles/tile/tile_style';
import { ITMSSource } from '../../sources/tms_source';
import { DataRequestContext } from '../../../actions';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
interface RasterTileSourceData {
url: string;
}
export class RasterTileLayer extends AbstractLayer {
static createDescriptor(options: Partial<LayerDescriptor>) {
@ -53,16 +58,33 @@ export class RasterTileLayer extends AbstractLayer {
}
async syncData({ startLoading, stopLoading, onLoadError, dataFilters }: DataRequestContext) {
const sourceDataRequest = this.getSourceDataRequest();
if (sourceDataRequest) {
// data is immmutable
return;
const source = this.getSource();
const nextMeta = {
...dataFilters,
applyGlobalTime: source.getApplyGlobalTime(),
};
const prevDataRequest = this.getSourceDataRequest();
if (prevDataRequest) {
const prevMeta = prevDataRequest?.getMeta();
const canSkip = await canSkipSourceUpdate({
extentAware: false,
source,
prevDataRequest,
nextRequestMeta: nextMeta,
getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
if (!prevMeta) return true;
return source.getUpdateDueToTimeslice(prevMeta, timeslice);
},
});
if (canSkip) return;
}
const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`);
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, dataFilters);
try {
const url = await this.getSource().getUrlTemplate();
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, { url }, {});
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, nextMeta);
const data: RasterTileSourceData = {
url: await source.getUrlTemplate(dataFilters),
};
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data, {});
} catch (error) {
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
}
@ -84,7 +106,26 @@ export class RasterTileLayer extends AbstractLayer {
return this.getId() === mbSourceId;
}
_requiresPrevSourceCleanup(mbMap: MbMap): boolean {
const mbSource = mbMap.getSource(this.getMbSourceId()) as RasterTileSource;
if (!mbSource) {
return false;
}
const sourceDataRequest = this.getSourceDataRequest();
if (!sourceDataRequest) {
return false;
}
const sourceData = sourceDataRequest.getData() as RasterTileSourceData | undefined;
if (!sourceData) {
return false;
}
return mbSource.tiles?.[0] !== sourceData.url;
}
syncLayerWithMB(mbMap: MbMap) {
this._removeStaleMbSourcesAndLayers(mbMap);
const source = mbMap.getSource(this.getId());
const mbLayerId = this._getMbLayerId();
@ -135,4 +176,8 @@ export class RasterTileLayer extends AbstractLayer {
isBasemap(order: number) {
return order === 0;
}
async isFilteredByGlobalTime(): Promise<boolean> {
return this.getSource().getApplyGlobalTime() && (await this.getSource().isTimeAware());
}
}

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { DataFilters } from '../../../../common/descriptor_types';
import { ISource } from '../source';
export interface ITMSSource extends ISource {
getUrlTemplate(): Promise<string>;
getUrlTemplate(dataFilters: DataFilters): Promise<string>;
}

View file

@ -34,6 +34,8 @@ export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './e
export type { EMSTermJoinConfig, SampleValuesConfig } from './ems_autosuggest';
export type { ITMSSource } from './classes/sources/tms_source';
export type {
GetFeatureActionsArgs,
IVectorSource,