GroundImage — pinning an image to the map
Define a rectangle with a south-west and a north-east point and lay one image inside it: floor plans, historical maps, aerial photography, your own colour-coded diagrams — anything that has not been cut into tiles. Bounds and opacity can be changed later, and the image stays glued to the ground as the camera moves.
01 · Ground image state
GroundImageState represents one image: bounds, the image itself, opacity and a tap handler. The image is passed in whatever form is natural per platform — a Drawable on Android, a UIImage on iOS, a URL on the web.
val groundImageState =
GroundImageState(
id = "groundImage",
bounds =
GeoRectBounds(
southWest = southWestPosition,
northEast = northEastPosition,
),
image = drawable, // Drawable をそのまま渡す
opacity = 0.5f,
onClick = this::onGroundImageClick,
)
GroundImage(groundImageState)let groundImageState = GroundImageState(
bounds: GeoRectBounds(southWest: southWest, northEast: northEast),
image: resources.image, // UIImage をそのまま渡す
opacity: 0.5,
id: "groundImage"
)
content.groundImages = [GroundImage(state: groundImageState)]const image = createGroundImageState({
id: 'ground-image',
bounds: createGeoRectBounds({ southWest, northEast }),
imageUrl: GROUND_IMAGE_URL, // URL で渡す
opacity,
onClick: () => showToast('Ground image clicked.'),
});
<GroundImage state={image} />02 · Bounds and opacity
The bounds are a rectangle. If the image aspect ratio differs from the bounds, the image is stretched to fit, so match the bounds to the aspect of the plan you are placing. Bounds, opacity and the image can all be reassigned and apply immediately.
Rebuild the bounds by dragging
Blend with the base map

// 四隅のマーカーをドラッグして範囲を作り直す
override fun onMarkerDrag(dragged: MarkerState) {
when (dragged.id) {
"south_west" -> southWestPosition = GeoPoint.from(dragged.position)
"north_east" -> northEastPosition = GeoPoint.from(dragged.position)
}
bounds = GeoRectBounds(southWest = southWestPosition, northEast = northEastPosition)
}
// 透明度はそのまま代入するだけ
groundImageState.opacity = sliderOpacityfunc setOpacity(_ value: Double) {
opacity = value
groundImageState.opacity = value
}
// マーカーのドラッグに合わせて範囲を作り直す
groundImageState.bounds = GeoRectBounds(southWest: sw, northEast: ne)// ドラッグ中は状態オブジェクトを直接書き換える(再レンダリングを待たない)
const applyGeometry = (sw: GeoPoint, ne: GeoPoint) => {
image.bounds = createGeoRectBounds({ southWest: sw, northEast: ne });
polylineState.points = framePoints(sw, ne);
};
image.opacity = opacity;03 · The same result on every provider
GroundImage is the feature with the widest implementation gap between map SDK drivers. Overlay mechanisms differ greatly and some engines have no equivalent at all. MapConductor absorbs as much of that as it can, but some providers keep limits on dynamic changes.
Same bounds, same stretch
The image is fitted to the bounds you gave, regardless of how the engine models overlay coordinates — same position, same size everywhere.
Native primitives first
Each driver draws through the map SDK own image-overlay primitive wherever one exists — MediaLayer with ImageElement on ArcGIS, an image-material Rectangle on Cesium, and so on. Only where no equivalent exists is the image sliced into tiles behind a raster layer. Your code is the same either way.
Per-provider limits
Rebuilding the bounds (continuous updates during a drag) and changing opacity can be limited by the engine. Static placement is consistent everywhere, but verify on your target providers if you change these dynamically.
Shared tap detection
Tap detection is shared logic, so onClick fires whether or not the engine supports taps on image overlays.
04 · Tapping
onClick receives the image state and the tapped coordinate. The samples swap the image itself on tap to toggle between two versions.
// タップで画像を差し替える
override fun onGroundImageClick(clicked: GroundImageEvent) {
val isBase = clicked.state.image == imageResources.image
image = if (isBase) imageResources.clickedImage else imageResources.image
}private func onGroundImageClick(_ event: GroundImageEvent) {
let isBase = groundImageState.image === resources.image
groundImageState.image = isBase ? resources.clickedImage : resources.image
}createGroundImageState({
bounds,
imageUrl,
onClick: event => showToast(event.clicked),
});Transparent parts of the image still count as tappable. To make them inert, match the bounds to the image content or pair it with a polygon.