Geomap: use name as UID (#41668)

This commit is contained in:
Ryan McKinley
2021-11-15 11:18:08 -08:00
committed by GitHub
parent 1f07d32666
commit 466eaeb4f0
10 changed files with 108 additions and 110 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ export interface FrameGeometrySource {
*/ */
export interface MapLayerOptions<TConfig = any> { export interface MapLayerOptions<TConfig = any> {
type: string; type: string;
name?: string; // configured unique display name name: string; // configured unique display name
// Custom options depending on the type // Custom options depending on the type
config?: TConfig; config?: TConfig;
+76 -69
View File
@@ -1,6 +1,6 @@
import React, { Component, ReactNode } from 'react'; import React, { Component, ReactNode } from 'react';
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry'; import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
import { Map, MapBrowserEvent, View } from 'ol'; import { Map as OpenLayersMap, MapBrowserEvent, View } from 'ol';
import Attribution from 'ol/control/Attribution'; import Attribution from 'ol/control/Attribution';
import Zoom from 'ol/control/Zoom'; import Zoom from 'ol/control/Zoom';
import ScaleLine from 'ol/control/ScaleLine'; import ScaleLine from 'ol/control/ScaleLine';
@@ -46,13 +46,13 @@ interface State extends OverlayProps {
export interface GeomapLayerActions { export interface GeomapLayerActions {
selectLayer: (uid: string) => void; selectLayer: (uid: string) => void;
deleteLayer: (uid: string) => void; deleteLayer: (uid: string) => void;
updateLayer: (uid: string, updatedLayer: MapLayerState<any>) => void;
addlayer: (type: string) => void; addlayer: (type: string) => void;
reorder: (src: number, dst: number) => void; reorder: (src: number, dst: number) => void;
canRename: (v: string) => boolean;
} }
export interface GeomapInstanceState { export interface GeomapInstanceState {
map?: Map; map?: OpenLayersMap;
layers: MapLayerState[]; layers: MapLayerState[];
selected: number; selected: number;
actions: GeomapLayerActions; actions: GeomapLayerActions;
@@ -65,15 +65,15 @@ export class GeomapPanel extends Component<Props, State> {
globalCSS = getGlobalStyles(config.theme2); globalCSS = getGlobalStyles(config.theme2);
counter = 0;
mouseWheelZoom?: MouseWheelZoom; mouseWheelZoom?: MouseWheelZoom;
style = getStyles(config.theme); style = getStyles(config.theme);
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 }; hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
readonly hoverEvent = new DataHoverEvent(this.hoverPayload); readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
map?: Map; map?: OpenLayersMap;
mapDiv?: HTMLDivElement; mapDiv?: HTMLDivElement;
layers: MapLayerState[] = []; layers: MapLayerState[] = [];
readonly byName = new Map<string, MapLayerState>();
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -109,6 +109,7 @@ export class GeomapPanel extends Component<Props, State> {
return true; // always? return true; // always?
} }
/** This funciton will actually update the JSON model */
private doOptionsUpdate(selected: number) { private doOptionsUpdate(selected: number) {
const { options, onOptionsChange } = this.props; const { options, onOptionsChange } = this.props;
const layers = this.layers; const layers = this.layers;
@@ -129,9 +130,20 @@ export class GeomapPanel extends Component<Props, State> {
} }
} }
getNextLayerName = () => {
let idx = this.layers.length; // since basemap is 0, this looks right
while (true && idx < 100) {
const name = `Layer ${idx++}`;
if (!this.byName.has(name)) {
return name;
}
}
return `Layer ${Date.now()}`;
};
actions: GeomapLayerActions = { actions: GeomapLayerActions = {
selectLayer: (uid: string) => { selectLayer: (uid: string) => {
const selected = this.layers.findIndex((v) => v.UID === uid); const selected = this.layers.findIndex((v) => v.options.name === uid);
if (this.panelContext.onInstanceStateChange) { if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({ this.panelContext.onInstanceStateChange({
map: this.map, map: this.map,
@@ -141,10 +153,13 @@ export class GeomapPanel extends Component<Props, State> {
}); });
} }
}, },
canRename: (v: string) => {
return !this.byName.has(v);
},
deleteLayer: (uid: string) => { deleteLayer: (uid: string) => {
const layers: MapLayerState[] = []; const layers: MapLayerState[] = [];
for (const lyr of this.layers) { for (const lyr of this.layers) {
if (lyr.UID === uid) { if (lyr.options.name === uid) {
this.map?.removeLayer(lyr.layer); this.map?.removeLayer(lyr.layer);
} else { } else {
layers.push(lyr); layers.push(lyr);
@@ -153,21 +168,6 @@ export class GeomapPanel extends Component<Props, State> {
this.layers = layers; this.layers = layers;
this.doOptionsUpdate(0); this.doOptionsUpdate(0);
}, },
updateLayer: (uid: string, updatedLayer: MapLayerState<any>) => {
const selected = this.layers.findIndex((v) => v.UID === uid);
const layers: MapLayerState[] = [];
for (const lyr of this.layers) {
if (lyr.UID === uid) {
this.map?.removeLayer(lyr.layer);
this.map?.addLayer(updatedLayer.layer);
layers.push(updatedLayer);
} else {
layers.push(lyr);
}
}
this.layers = layers;
this.doOptionsUpdate(selected);
},
addlayer: (type: string) => { addlayer: (type: string) => {
const item = geomapLayerRegistry.getIfExists(type); const item = geomapLayerRegistry.getIfExists(type);
if (!item) { if (!item) {
@@ -177,6 +177,7 @@ export class GeomapPanel extends Component<Props, State> {
this.map!, this.map!,
{ {
type: item.id, type: item.id,
name: this.getNextLayerName(),
config: cloneDeep(item.defaultOptions), config: cloneDeep(item.defaultOptions),
}, },
false false
@@ -195,6 +196,11 @@ export class GeomapPanel extends Component<Props, State> {
this.layers = result; this.layers = result;
this.doOptionsUpdate(endIndex); this.doOptionsUpdate(endIndex);
// Add the layers in the right order
const group = this.map?.getLayers()!;
group.clear();
this.layers.forEach((v) => group.push(v.layer));
}, },
}; };
@@ -236,12 +242,12 @@ export class GeomapPanel extends Component<Props, State> {
} }
if (!div) { if (!div) {
this.map = (undefined as unknown) as Map; this.map = (undefined as unknown) as OpenLayersMap;
return; return;
} }
const { options } = this.props; const { options } = this.props;
const map = (this.map = new Map({ const map = (this.map = new OpenLayersMap({
view: this.initMapView(options.view), view: this.initMapView(options.view),
pixelRatio: 1, // or zoom? pixelRatio: 1, // or zoom?
layers: [], // loaded explicitly below layers: [], // loaded explicitly below
@@ -252,6 +258,7 @@ export class GeomapPanel extends Component<Props, State> {
}), }),
})); }));
this.byName.clear();
const layers: MapLayerState[] = []; const layers: MapLayerState[] = [];
try { try {
layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true)); layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
@@ -354,28 +361,46 @@ export class GeomapPanel extends Component<Props, State> {
if (!this.map) { if (!this.map) {
return false; return false;
} }
const selected = this.layers.findIndex((v) => v.UID === uid); const current = this.byName.get(uid);
if (selected < 0) { if (!current) {
return false; return false;
} }
const layers = this.layers.slice(0);
try { let layerIndex = -1;
let found = false; const group = this.map?.getLayers()!;
const current = this.layers[selected]; for (let i = 0; i < group?.getLength(); i++) {
const info = await this.initLayer(this.map, newOptions, current.isBasemap); if (group.item(i) === current.layer) {
const group = this.map?.getLayers()!; layerIndex = i;
for (let i = 0; i < group?.getLength(); i++) { break;
if (group.item(i) === current.layer) {
found = true;
group.setAt(i, info.layer);
break;
}
} }
if (!found) { }
console.warn('ERROR not found', uid);
// Special handling for rename
if (newOptions.name !== uid) {
if (!newOptions.name) {
newOptions.name = uid;
} else if (this.byName.has(newOptions.name)) {
return false; return false;
} }
layers[selected] = info; console.log('Layer name changed', uid, '>>>', newOptions.name);
this.byName.delete(uid);
uid = newOptions.name;
this.byName.set(uid, current);
}
// Type changed -- requires full re-initalization
if (current.options.type !== newOptions.type) {
// full init
} else {
// just update options
}
const layers = this.layers.slice(0);
try {
const info = await this.initLayer(this.map, newOptions, current.isBasemap);
layers[layerIndex] = info;
group.setAt(layerIndex, info.layer);
// initialize with new data // initialize with new data
if (info.handler.update) { if (info.handler.update) {
@@ -385,28 +410,13 @@ export class GeomapPanel extends Component<Props, State> {
console.warn('ERROR', err); console.warn('ERROR', err);
return false; return false;
} }
// TODO
// validate names, basemap etc
this.layers = layers; this.layers = layers;
this.doOptionsUpdate(selected); this.doOptionsUpdate(layerIndex);
return true; return true;
}; };
private generateLayerName = (): string => { async initLayer(map: OpenLayersMap, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
let newLayerName = `Layer ${this.counter}`;
for (const otherLayer of this.layers) {
if (newLayerName === otherLayer.options.name) {
newLayerName += '-1';
}
}
return newLayerName;
};
async initLayer(map: Map, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) { if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
options = DEFAULT_BASEMAP_CONFIG; options = DEFAULT_BASEMAP_CONFIG;
} }
@@ -415,6 +425,7 @@ export class GeomapPanel extends Component<Props, State> {
if (!options?.type) { if (!options?.type) {
options = { options = {
type: MARKERS_LAYER_ID, type: MARKERS_LAYER_ID,
name: this.getNextLayerName(),
config: {}, config: {},
}; };
} }
@@ -427,32 +438,28 @@ export class GeomapPanel extends Component<Props, State> {
const handler = await item.create(map, options, config.theme2); const handler = await item.create(map, options, config.theme2);
const layer = handler.init(); const layer = handler.init();
// const key = layer.on('change', () => {
// const state = layer.getLayerState();
// console.log('LAYER', key, state);
// });
if (handler.update) { if (handler.update) {
handler.update(this.props.data); handler.update(this.props.data);
} }
if (!options.name) { if (!options.name) {
options.name = this.generateLayerName(); options.name = this.getNextLayerName();
} }
const UID = `lyr-${this.counter++}`; const UID = options.name;
const state = {
return { UID, // unique name when added to the map (it may change and will need special handling)
UID,
isBasemap, isBasemap,
options, options,
layer, layer,
handler, handler,
// Used by the editors // Used by the editors
onChange: (cfg) => { onChange: (cfg: MapLayerOptions) => {
this.updateLayer(UID, cfg); this.updateLayer(UID, cfg);
}, },
}; };
this.byName.set(UID, state);
return state;
} }
initMapView(config: MapViewConfig): View { initMapView(config: MapViewConfig): View {
@@ -11,7 +11,7 @@ describe('LayerHeader', () => {
fireEvent.change(input, { target: { value: 'new name' } }); fireEvent.change(input, { target: { value: 'new name' } });
fireEvent.blur(input); fireEvent.blur(input);
expect((scenario.props.onChange as any).mock.calls[0][0].options.name).toBe('new name'); expect((scenario.props.onChange as any).mock.calls[0][0].name).toBe('new name');
}); });
it('Show error when empty name is specified', async () => { it('Show error when empty name is specified', async () => {
@@ -37,21 +37,12 @@ describe('LayerHeader', () => {
}); });
function renderScenario(overrides: Partial<LayerHeaderProps>) { function renderScenario(overrides: Partial<LayerHeaderProps>) {
const props: any = { const props: LayerHeaderProps = {
layer: { layer: { name: 'Layer 1', type: '?' },
UID: '1', canRename: (v: string) => {
options: { name: 'Layer 1' }, const names = new Set(['Layer 1', 'Layer 2']);
return !names.has(v);
}, },
layers: [
{
UID: '1',
options: { name: 'Layer 1' },
},
{
UID: '2',
options: { name: 'Layer 2' },
},
],
onChange: jest.fn(), onChange: jest.fn(),
}; };
@@ -1,17 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui'; import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme, MapLayerOptions } from '@grafana/data';
import { MapLayerState } from '../../types';
export interface LayerHeaderProps { export interface LayerHeaderProps {
layer: MapLayerState<any>; layer: MapLayerOptions<any>;
layers: Array<MapLayerState<any>>; canRename: (v: string) => boolean;
onChange: (layer: MapLayerState<any>) => void; onChange: (layer: MapLayerOptions<any>) => void;
} }
export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => { export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
@@ -29,10 +27,10 @@ export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => {
return; return;
} }
if (layer.options.name !== newName) { if (layer.name !== newName) {
onChange({ onChange({
...layer, ...layer,
options: { ...layer.options, name: newName }, name: newName,
}); });
} }
}; };
@@ -45,11 +43,9 @@ export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => {
return; return;
} }
for (const otherLayer of layers) { if (!canRename(newName)) {
if (otherLayer.UID !== layer.UID && newName === otherLayer.options.name) { setValidationError('Layer name already exists');
setValidationError('Layer name already exists'); return;
return;
}
} }
if (validationError) { if (validationError) {
@@ -81,7 +77,7 @@ export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => {
onClick={onEditLayer} onClick={onEditLayer}
data-testid="layer-name-div" data-testid="layer-name-div"
> >
<span className={styles.layerName}>{layer.options.name}</span> <span className={styles.layerName}>{layer.name}</span>
<Icon name="pen" className={styles.layerEditIcon} size="sm" /> <Icon name="pen" className={styles.layerEditIcon} size="sm" />
</button> </button>
)} )}
@@ -90,7 +86,7 @@ export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => {
<> <>
<Input <Input
type="text" type="text"
defaultValue={layer.options.name} defaultValue={layer.name}
onBlur={onEditLayerBlur} onBlur={onEditLayerBlur}
autoFocus autoFocus
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
@@ -23,10 +23,6 @@ export const LayerList = ({ layers, onDragEnd, selected, actions }: LayerListPro
return sel ? `${style.row} ${style.sel}` : style.row; return sel ? `${style.row} ${style.sel}` : style.row;
}; };
const onLayerNameChange = (layer: MapLayerState<any>) => {
actions.updateLayer(layer.UID, layer);
};
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable"> <Droppable droppableId="droppable">
@@ -37,24 +33,29 @@ export const LayerList = ({ layers, onDragEnd, selected, actions }: LayerListPro
const rows: any = []; const rows: any = [];
for (let i = layers.length - 1; i > 0; i--) { for (let i = layers.length - 1; i > 0; i--) {
const element = layers[i]; const element = layers[i];
const uid = element.options.name;
rows.push( rows.push(
<Draggable key={element.UID} draggableId={element.UID} index={rows.length}> <Draggable key={uid} draggableId={uid} index={rows.length}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={getRowStyle(i === selected)} className={getRowStyle(i === selected)}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
onMouseDown={() => actions!.selectLayer(element.UID)} onMouseDown={() => actions!.selectLayer(uid)}
> >
<LayerHeader layer={{ ...element }} layers={layers} onChange={onLayerNameChange} /> <LayerHeader
layer={element.options}
canRename={actions.canRename}
onChange={element.onChange}
/>
<div className={style.textWrapper}>&nbsp; {element.options.type}</div> <div className={style.textWrapper}>&nbsp; {element.options.type}</div>
<IconButton <IconButton
name="trash-alt" name="trash-alt"
title={'remove'} title={'remove'}
className={cx(style.actionIcon, style.dragIcon)} className={cx(style.actionIcon, style.dragIcon)}
onClick={() => actions.deleteLayer(element.UID)} onClick={() => actions.deleteLayer(uid)}
surface="header" surface="header"
/> />
{layers.length > 2 && ( {layers.length > 2 && (
@@ -41,6 +41,7 @@ export const MARKERS_LAYER_ID = 'markers';
// Used by default when nothing is configured // Used by default when nothing is configured
export const defaultMarkersConfig: MapLayerOptions<MarkersConfig> = { export const defaultMarkersConfig: MapLayerOptions<MarkersConfig> = {
type: MARKERS_LAYER_ID, type: MARKERS_LAYER_ID,
name: '', // will get replaced
config: defaultOptions, config: defaultOptions,
location: { location: {
mode: FrameGeometrySourceMode.Auto, mode: FrameGeometrySourceMode.Auto,
@@ -7,6 +7,7 @@ import { dataLayers } from './data';
export const DEFAULT_BASEMAP_CONFIG: MapLayerOptions = { export const DEFAULT_BASEMAP_CONFIG: MapLayerOptions = {
type: 'default', type: 'default',
name: '', // will get filled in with a non-empty name
config: {}, config: {},
}; };
@@ -47,6 +47,7 @@ describe('Worldmap Migrations', () => {
}, },
"options": Object { "options": Object {
"basemap": Object { "basemap": Object {
"name": "Basemap",
"type": "default", "type": "default",
}, },
"controls": Object { "controls": Object {
@@ -40,6 +40,7 @@ export function worldmapToGeomapOptions(angular: any): { fieldConfig: FieldConfi
}, },
basemap: { basemap: {
type: 'default', // was carto type: 'default', // was carto
name: 'Basemap',
}, },
layers: [ layers: [
// TODO? depends on current configs // TODO? depends on current configs
-1
View File
@@ -71,7 +71,6 @@ export interface GazetteerPathEditorConfigSettings {
// Runtime model // Runtime model
//------------------- //-------------------
export interface MapLayerState<TConfig = any> { export interface MapLayerState<TConfig = any> {
UID: string; // value changes with each initialization
options: MapLayerOptions<TConfig>; options: MapLayerOptions<TConfig>;
handler: MapLayerHandler; handler: MapLayerHandler;
layer: BaseLayer; // the openlayers instance layer: BaseLayer; // the openlayers instance