mirror of https://github.com/grafana/grafana.git
Geomap: Add option to toggle no-repeating (#108201)
* Geomap: Add option to toggle no-repeating * Add option and apply to all basemaps * Clean up some comments * Update docs * Add tests * Fix option change handling issues * Update translations * Fix e2e test
This commit is contained in:
parent
4b9e03e7c0
commit
bee169d7a6
|
|
@ -184,6 +184,14 @@ The **Share view** option allows you to link the movement and zoom actions of mu
|
|||
You might need to reload the dashboard for this feature to work.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### No map repeating
|
||||
|
||||
The **No map repeating** option prevents the base map tiles from repeating horizontally when you pan across the world. This constrains the view to a single instance of the world map and avoids visual confusion when displaying global datasets.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Enabling this option requires the map to reinitialize.
|
||||
{{< /admonition >}}
|
||||
|
||||
### Map layers options
|
||||
|
||||
Geomaps support showing multiple layers. Each layer determines how you visualize geospatial data on top of the base map.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('Geomap layer types', () => {
|
|||
|
||||
it('Tests changing the layer type', () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').scrollIntoView().should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('markers');
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface MapViewConfig {
|
|||
lon?: number;
|
||||
maxZoom?: number;
|
||||
minZoom?: number;
|
||||
noRepeat?: boolean;
|
||||
padding?: number;
|
||||
shared?: boolean;
|
||||
zoom?: number;
|
||||
|
|
@ -43,6 +44,7 @@ export const defaultMapViewConfig: Partial<MapViewConfig> = {
|
|||
id: 'zero',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
noRepeat: false,
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export interface MapLayerOptions<TConfig = any> extends raw.MapLayerOptions {
|
|||
// Custom options depending on the type
|
||||
config?: TConfig;
|
||||
filterData?: MatcherConfig;
|
||||
// Disable world repetition for basemap layers
|
||||
noRepeat?: boolean;
|
||||
}
|
||||
|
||||
export interface DataQuery extends raw.DataQuery {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { css } from '@emotion/css';
|
|||
import { Global } from '@emotion/react';
|
||||
import OpenLayersMap from 'ol/Map';
|
||||
import MapBrowserEvent from 'ol/MapBrowserEvent';
|
||||
import View from 'ol/View';
|
||||
import View, { ViewOptions } from 'ol/View';
|
||||
import Attribution from 'ol/control/Attribution';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { fromLonLat, transformExtent } from 'ol/proj';
|
||||
import { Component, ReactNode } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
|
@ -132,11 +132,6 @@ export class GeomapPanel extends Component<Props, State> {
|
|||
this.dataChanged(nextProps.data);
|
||||
}
|
||||
|
||||
// Options changed
|
||||
if (this.props.options !== nextProps.options) {
|
||||
this.optionsChanged(nextProps.options);
|
||||
}
|
||||
|
||||
return true; // always?
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +143,10 @@ export class GeomapPanel extends Component<Props, State> {
|
|||
if (this.map && this.props.data !== prevProps.data) {
|
||||
this.dataChanged(this.props.data);
|
||||
}
|
||||
// Handle options changes
|
||||
if (this.props.options !== prevProps.options) {
|
||||
this.optionsChanged(prevProps.options, this.props.options);
|
||||
}
|
||||
}
|
||||
|
||||
/** This function will actually update the JSON model */
|
||||
|
|
@ -177,18 +176,29 @@ export class GeomapPanel extends Component<Props, State> {
|
|||
*
|
||||
* NOTE: changes to basemap and layers are handled independently
|
||||
*/
|
||||
optionsChanged(options: Options) {
|
||||
const oldOptions = this.props.options;
|
||||
if (options.view !== oldOptions.view) {
|
||||
const view = this.initMapView(options.view);
|
||||
optionsChanged(oldOptions: Options, newOptions: Options) {
|
||||
// First check if noRepeat changed - requires full map reinitialization
|
||||
const noRepeatChanged = oldOptions.view?.noRepeat !== newOptions.view?.noRepeat;
|
||||
|
||||
if (noRepeatChanged) {
|
||||
if (this.mapDiv) {
|
||||
this.initMapRef(this.mapDiv);
|
||||
}
|
||||
// Skip other options processing
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle incremental view changes
|
||||
if (oldOptions.view !== newOptions.view) {
|
||||
const view = this.initMapView(newOptions.view);
|
||||
if (this.map && view) {
|
||||
this.map.setView(view);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.controls !== oldOptions.controls) {
|
||||
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
|
||||
// Handle controls changes
|
||||
if (newOptions.controls !== oldOptions.controls) {
|
||||
this.initControls(newOptions.controls ?? { showZoom: true, showAttribution: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +244,12 @@ export class GeomapPanel extends Component<Props, State> {
|
|||
this.byName.clear();
|
||||
const layers: MapLayerState[] = [];
|
||||
try {
|
||||
layers.push(await initLayer(this, map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||
// Pass noRepeat setting to basemap layer
|
||||
const basemapOptions = {
|
||||
...(options.basemap ?? DEFAULT_BASEMAP_CONFIG),
|
||||
noRepeat: options.view?.noRepeat ?? false,
|
||||
};
|
||||
layers.push(await initLayer(this, map, basemapOptions, true));
|
||||
|
||||
// Default layer values
|
||||
if (!options.layers) {
|
||||
|
|
@ -284,11 +299,24 @@ export class GeomapPanel extends Component<Props, State> {
|
|||
};
|
||||
|
||||
initMapView = (config: MapViewConfig): View | undefined => {
|
||||
let view = new View({
|
||||
const noRepeat = config.noRepeat ?? false;
|
||||
|
||||
let viewOptions: ViewOptions = {
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
showFullExtent: true, // allows zooming so the full range is visible
|
||||
});
|
||||
};
|
||||
|
||||
// Only apply constraints when no-repeat is enabled
|
||||
if (noRepeat) {
|
||||
// Define the world extent in EPSG:3857 (Web Mercator)
|
||||
const worldExtent = [-180, -85.05112878, 180, 85.05112878]; // [minx, miny, maxx, maxy] in EPSG:4326
|
||||
const projectedExtent = transformExtent(worldExtent, 'EPSG:4326', 'EPSG:3857');
|
||||
viewOptions.extent = projectedExtent;
|
||||
viewOptions.showFullExtent = false;
|
||||
viewOptions.constrainOnlyCenter = false;
|
||||
}
|
||||
|
||||
let view = new View(viewOptions);
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import OpenLayersMap from 'ol/Map';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
|
||||
import { EventBus, GrafanaTheme2, MapLayerOptions } from '@grafana/data';
|
||||
|
||||
import { carto, CartoConfig, LayerTheme } from './carto';
|
||||
|
||||
describe('CARTO basemap layer noRepeat functionality', () => {
|
||||
let mockMap: OpenLayersMap;
|
||||
let mockEventBus: EventBus;
|
||||
let mockTheme: GrafanaTheme2;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMap = {} as OpenLayersMap;
|
||||
mockEventBus = {} as EventBus;
|
||||
mockTheme = { isDark: false } as GrafanaTheme2;
|
||||
});
|
||||
|
||||
it('should set wrapX to false when noRepeat is true', async () => {
|
||||
const options: MapLayerOptions<CartoConfig> = {
|
||||
name: 'Test CARTO Layer',
|
||||
type: 'carto',
|
||||
config: {
|
||||
theme: LayerTheme.Light,
|
||||
showLabels: true,
|
||||
},
|
||||
noRepeat: true,
|
||||
};
|
||||
|
||||
const result = await carto.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is false', async () => {
|
||||
const options: MapLayerOptions<CartoConfig> = {
|
||||
name: 'Test CARTO Layer',
|
||||
type: 'carto',
|
||||
config: {
|
||||
theme: LayerTheme.Dark,
|
||||
showLabels: false,
|
||||
},
|
||||
noRepeat: false,
|
||||
};
|
||||
|
||||
const result = await carto.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is undefined (defaults to false)', async () => {
|
||||
const options: MapLayerOptions<CartoConfig> = {
|
||||
name: 'Test CARTO Layer',
|
||||
type: 'carto',
|
||||
config: {
|
||||
theme: LayerTheme.Auto,
|
||||
showLabels: true,
|
||||
},
|
||||
// noRepeat not specified
|
||||
};
|
||||
|
||||
const result = await carto.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve theme and label settings when noRepeat is set', async () => {
|
||||
const mockDarkTheme = { isDark: true } as GrafanaTheme2;
|
||||
const options: MapLayerOptions<CartoConfig> = {
|
||||
name: 'Test CARTO Layer',
|
||||
type: 'carto',
|
||||
config: {
|
||||
theme: LayerTheme.Auto, // Should use dark theme from mockDarkTheme
|
||||
showLabels: false,
|
||||
},
|
||||
noRepeat: true,
|
||||
};
|
||||
|
||||
const result = await carto.create(mockMap, options, mockEventBus, mockDarkTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source.getWrapX()).toBe(false);
|
||||
|
||||
// Check that the URL reflects the dark theme without labels
|
||||
const urls = source.getUrls();
|
||||
expect(urls?.[0]).toContain('dark_nolabels');
|
||||
});
|
||||
});
|
||||
|
|
@ -51,10 +51,13 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
|||
style += '_nolabels';
|
||||
}
|
||||
const scale = window.devicePixelRatio > 1 ? '@2x' : '';
|
||||
const noRepeat = options.noRepeat ?? false;
|
||||
|
||||
return new TileLayer({
|
||||
source: new XYZ({
|
||||
attributions: `<a href="https://carto.com/attribution/">©CARTO</a> <a href="https://www.openstreetmap.org/copyright">©OpenStreetMap</a> contributors`,
|
||||
url: `https://{1-4}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}${scale}.png`,
|
||||
wrapX: !noRepeat,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import OpenLayersMap from 'ol/Map';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
|
||||
import { EventBus, GrafanaTheme2, MapLayerOptions } from '@grafana/data';
|
||||
|
||||
import { xyzTiles, XYZConfig } from './generic';
|
||||
|
||||
describe('XYZ tile layer noRepeat functionality', () => {
|
||||
let mockMap: OpenLayersMap;
|
||||
let mockEventBus: EventBus;
|
||||
let mockTheme: GrafanaTheme2;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMap = {} as OpenLayersMap;
|
||||
mockEventBus = {} as EventBus;
|
||||
mockTheme = {} as GrafanaTheme2;
|
||||
});
|
||||
|
||||
it('should set wrapX to false when noRepeat is true', async () => {
|
||||
const options: MapLayerOptions<XYZConfig> = {
|
||||
name: 'Test Layer',
|
||||
type: 'xyz',
|
||||
config: {
|
||||
url: 'https://example.com/{z}/{x}/{y}.png',
|
||||
attribution: 'Test Attribution',
|
||||
},
|
||||
noRepeat: true,
|
||||
};
|
||||
|
||||
const result = await xyzTiles.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is false', async () => {
|
||||
const options: MapLayerOptions<XYZConfig> = {
|
||||
name: 'Test Layer',
|
||||
type: 'xyz',
|
||||
config: {
|
||||
url: 'https://example.com/{z}/{x}/{y}.png',
|
||||
attribution: 'Test Attribution',
|
||||
},
|
||||
noRepeat: false,
|
||||
};
|
||||
|
||||
const result = await xyzTiles.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is undefined (defaults to false)', async () => {
|
||||
const options: MapLayerOptions<XYZConfig> = {
|
||||
name: 'Test Layer',
|
||||
type: 'xyz',
|
||||
config: {
|
||||
url: 'https://example.com/{z}/{x}/{y}.png',
|
||||
attribution: 'Test Attribution',
|
||||
},
|
||||
// noRepeat not specified
|
||||
};
|
||||
|
||||
const result = await xyzTiles.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source).toBeInstanceOf(XYZ);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve other layer properties when noRepeat is set', async () => {
|
||||
const options: MapLayerOptions<XYZConfig> = {
|
||||
name: 'Test Layer',
|
||||
type: 'xyz',
|
||||
config: {
|
||||
url: 'https://example.com/{z}/{x}/{y}.png',
|
||||
attribution: 'Test Attribution',
|
||||
minZoom: 2,
|
||||
maxZoom: 18,
|
||||
},
|
||||
noRepeat: true,
|
||||
};
|
||||
|
||||
const result = await xyzTiles.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
expect(layer.getMinZoom()).toBe(2);
|
||||
expect(layer.getMaxZoom()).toBe(18);
|
||||
|
||||
const source = (layer as TileLayer<XYZ>).getSource() as XYZ;
|
||||
expect(source.getWrapX()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -35,10 +35,13 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
|||
cfg.url = defaultXYZConfig.url;
|
||||
cfg.attribution = cfg.attribution ?? defaultXYZConfig.attribution;
|
||||
}
|
||||
const noRepeat = options.noRepeat ?? false;
|
||||
|
||||
return new TileLayer({
|
||||
source: new XYZ({
|
||||
url: cfg.url,
|
||||
attributions: cfg.attribution, // singular?
|
||||
wrapX: !noRepeat,
|
||||
}),
|
||||
minZoom: cfg.minZoom,
|
||||
maxZoom: cfg.maxZoom,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import OpenLayersMap from 'ol/Map';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import OSM from 'ol/source/OSM';
|
||||
|
||||
import { EventBus, MapLayerOptions, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { standard } from './osm';
|
||||
|
||||
describe('OSM layer noRepeat functionality', () => {
|
||||
let mockMap: OpenLayersMap;
|
||||
let mockEventBus: EventBus;
|
||||
let mockTheme: GrafanaTheme2;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMap = {} as OpenLayersMap;
|
||||
mockEventBus = {} as EventBus;
|
||||
mockTheme = {} as GrafanaTheme2;
|
||||
});
|
||||
|
||||
it('should set wrapX to false when noRepeat is true', async () => {
|
||||
const options: MapLayerOptions = {
|
||||
name: 'Test OSM Layer',
|
||||
type: 'osm-standard',
|
||||
noRepeat: true,
|
||||
};
|
||||
|
||||
const result = await standard.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<OSM>).getSource() as OSM;
|
||||
expect(source).toBeInstanceOf(OSM);
|
||||
expect(source.getWrapX()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is false', async () => {
|
||||
const options: MapLayerOptions = {
|
||||
name: 'Test OSM Layer',
|
||||
type: 'osm-standard',
|
||||
noRepeat: false,
|
||||
};
|
||||
|
||||
const result = await standard.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<OSM>).getSource() as OSM;
|
||||
expect(source).toBeInstanceOf(OSM);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set wrapX to true when noRepeat is undefined (defaults to false)', async () => {
|
||||
const options: MapLayerOptions = {
|
||||
name: 'Test OSM Layer',
|
||||
type: 'osm-standard',
|
||||
// noRepeat not specified
|
||||
};
|
||||
|
||||
const result = await standard.create(mockMap, options, mockEventBus, mockTheme);
|
||||
const layer = result.init();
|
||||
|
||||
expect(layer).toBeInstanceOf(TileLayer);
|
||||
const source = (layer as TileLayer<OSM>).getSource() as OSM;
|
||||
expect(source).toBeInstanceOf(OSM);
|
||||
expect(source.getWrapX()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -16,8 +16,10 @@ export const standard: MapLayerRegistryItem = {
|
|||
*/
|
||||
create: async (map: OpenLayersMap, options: MapLayerOptions, eventBus: EventBus) => ({
|
||||
init: () => {
|
||||
const noRepeat = options.noRepeat ?? false;
|
||||
|
||||
return new TileLayer({
|
||||
source: new OSM(),
|
||||
source: new OSM({ wrapX: !noRepeat }),
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -248,4 +248,31 @@ describe('geomap migrations', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
it('should handle migration when noRepeat is not set', () => {
|
||||
const panel = {
|
||||
id: 2,
|
||||
type: 'geomap',
|
||||
options: {
|
||||
view: {
|
||||
id: 'coords',
|
||||
zoom: 5,
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
type: 'markers',
|
||||
config: {
|
||||
showLegend: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pluginVersion: '8.2.0',
|
||||
} as PanelModel;
|
||||
|
||||
panel.options = mapMigrationHandler(panel);
|
||||
|
||||
expect(panel.options.view.noRepeat).toBeUndefined();
|
||||
expect(panel.options.view.id).toBe('coords');
|
||||
expect(panel.options.view.zoom).toBe(5);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel)
|
|||
defaultValue: defaultMapViewConfig.shared,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
category,
|
||||
path: 'view.noRepeat',
|
||||
name: t('geomap.name-no-repeat', 'No map repeating'),
|
||||
description: t('geomap.description-no-repeat', 'Prevent the map from repeating horizontally'),
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const state = context.instanceState as GeomapInstanceState;
|
||||
if (!state?.layers) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ composableKinds: PanelCfg: {
|
|||
lastOnly?: bool
|
||||
layer?: string
|
||||
shared?: bool
|
||||
noRepeat?: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
ControlsOptions: {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface MapViewConfig {
|
|||
lon?: number;
|
||||
maxZoom?: number;
|
||||
minZoom?: number;
|
||||
noRepeat?: boolean;
|
||||
padding?: number;
|
||||
shared?: boolean;
|
||||
zoom?: number;
|
||||
|
|
@ -41,6 +42,7 @@ export const defaultMapViewConfig: Partial<MapViewConfig> = {
|
|||
id: 'zero',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
noRepeat: false,
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7512,6 +7512,7 @@
|
|||
},
|
||||
"description-initial-view": "This location will show when the panel first loads.",
|
||||
"description-mouse-wheel-zoom": "Enable zoom control via mouse wheel",
|
||||
"description-no-repeat": "Prevent the map from repeating horizontally",
|
||||
"description-share-view": "Use the same view across multiple panels. Note: this may require a dashboard reload.",
|
||||
"description-show-attribution": "Show the map source attribution info in the lower right",
|
||||
"description-show-debug": "Show map info",
|
||||
|
|
@ -7571,6 +7572,7 @@
|
|||
},
|
||||
"name-initial-view": "Initial view",
|
||||
"name-mouse-wheel-zoom": "Mouse wheel zoom",
|
||||
"name-no-repeat": "No map repeating",
|
||||
"name-share-view": "Share view",
|
||||
"name-show-attribution": "Show attribution",
|
||||
"name-show-debug": "Show debug",
|
||||
|
|
|
|||
Loading…
Reference in New Issue