diff --git a/src/constants/src/default-settings.ts b/src/constants/src/default-settings.ts index 4b91c614a2..de3b5af7d1 100644 --- a/src/constants/src/default-settings.ts +++ b/src/constants/src/default-settings.ts @@ -1275,7 +1275,10 @@ export const GEOCODER_DATASET_NAME = 'geocoder_dataset'; export const GEOCODER_LAYER_ID = 'geocoder_layer'; export const GEOCODER_GEO_OFFSET = 0.05; export const GEOCODER_ICON_COLOR: [number, number, number] = [255, 0, 0]; -export const GEOCODER_ICON_SIZE = 80; +// Base icon size before anchor normalization. The geocoder pin icon is bottom-anchored +// and scaled to fit the ScatterplotLayer unit circle (~0.5x), so the rendered size is +// compensated here (80 * 2 = 160). +export const GEOCODER_ICON_SIZE = 160; // Editor export const EDITOR_LAYER_ID = 'kepler_editor_layer'; diff --git a/src/layers/src/icon-layer/icon-layer.ts b/src/layers/src/icon-layer/icon-layer.ts index b8ea7a6a7a..50b9bbff25 100644 --- a/src/layers/src/icon-layer/icon-layer.ts +++ b/src/layers/src/icon-layer/icon-layer.ts @@ -6,7 +6,7 @@ import {BrushingExtension} from '@deck.gl/extensions'; import {SvgIconLayer} from '@kepler.gl/deckgl-layers'; import IconLayerIcon from './icon-layer-icon'; -import {ICON_FIELDS, CULL_MODE} from '@kepler.gl/constants'; +import {ICON_FIELDS, CULL_MODE, GEOCODER_LAYER_ID} from '@kepler.gl/constants'; import IconInfoModalFactory from './icon-info-modal'; import Layer, {LayerBaseConfig, LayerBaseConfigPartial} from '../base-layer'; import {assignPointPairToLayerColumn, FindDefaultLayerPropsReturnValue} from '../layer-utils'; @@ -95,6 +95,8 @@ export const pointVisConfigs: { billboard: 'billboard' }; +const BOTTOM_ANCHOR_ICONS = ['place']; + function flatterIconPositions(icon) { // had to flip y, since @luma modal has changed return icon.mesh.cells.reduce((prev, cell) => { @@ -107,6 +109,41 @@ function flatterIconPositions(icon) { }, []); } +/** + * Anchors icon geometry at its bottom tip and normalizes it to fit within the + * ScatterplotLayer unit circle (radius ≤ 1). After shifting the tip to y=0, + * computes a uniform scale so the farthest vertex stays within the unit disk. + * GEOCODER_ICON_SIZE must compensate for this scale (original_size / scale). + */ +function anchorIconAtBottom(positions: number[]): number[] { + const anchored = positions.slice(); + let minY = Infinity; + for (let i = 1; i < anchored.length; i += 3) { + if (anchored[i] < minY) minY = anchored[i]; + } + // Shift tip to y=0 + for (let i = 1; i < anchored.length; i += 3) { + anchored[i] -= minY; + } + // Compute the maximum distance from origin across all vertices + let maxDist = 0; + for (let i = 0; i < anchored.length; i += 3) { + const x = anchored[i]; + const y = anchored[i + 1]; + const dist = Math.sqrt(x * x + y * y); + if (dist > maxDist) maxDist = dist; + } + // Normalize so all vertices fit within the unit circle + if (maxDist > 1) { + const scale = 1 / maxDist; + for (let i = 0; i < anchored.length; i += 3) { + anchored[i] *= scale; + anchored[i + 1] *= scale; + } + } + return anchored; +} + export default class IconLayer extends Layer { getIconAccessor: (dataContainer: DataContainerInterface) => (d: any) => any; _layerInfoModal: () => JSX.Element; @@ -421,6 +458,14 @@ export default class IconLayer extends Layer { const baseLayerId = defaultLayerProps.id || this.id; const layerIdWithVersion = `${baseLayerId}_${this.iconGeometryVersion}`; + const isGeocoderLayer = this.id === GEOCODER_LAYER_ID; + const getIconGeometry = isGeocoderLayer + ? (id: string) => { + const geo = this.iconGeometry?.[id]; + return geo && BOTTOM_ANCHOR_ICONS.includes(id) ? anchorIconAtBottom(geo) : geo; + } + : (id: string) => this.iconGeometry?.[id]; + return [ new SvgIconLayer({ ...defaultLayerProps, @@ -429,7 +474,7 @@ export default class IconLayer extends Layer { ...layerProps, ...data, parameters, - getIconGeometry: id => this.iconGeometry?.[id], + getIconGeometry, // update triggers updateTriggers, @@ -449,7 +494,7 @@ export default class IconLayer extends Layer { getPosition: data.getPosition, getRadius: data.getRadius, getFillColor: this.config.highlightColor, - getIconGeometry: id => this.iconGeometry?.[id] + getIconGeometry }) ] : []), diff --git a/test/browser/components/geocoder-panel-test.js b/test/browser/components/geocoder-panel-test.js index 52653a0107..c22eb2e8d4 100644 --- a/test/browser/components/geocoder-panel-test.js +++ b/test/browser/components/geocoder-panel-test.js @@ -117,7 +117,7 @@ test('GeocoderPanel - render', t => { isVisible: true, hidden: true, visConfig: { - radius: 80 + radius: 160 } } }