maplibre-react-native
maplibre-react-native copied to clipboard
Heatmaps shape issue for react-native on iOS, and behaviour during zooming differs from android
Describe and reproduce the Bug
Describe the bug
A clear and concise description of what the bug is.
To Reproduce
Steps to reproduce the behavior:
- Create a heat map layer using react-native
- Display it on the map
- view it on android (Works well, scales nicely with zoom, looks good no issues)
- view it on iOS (Looks bad, the points are too intense and cannot be winded down, zooming causes the points to reset size and grow)
Expected behavior
It should look the same on iOS as it does on android
Android:
where as on iOS it looks like this with the same settings:
also when zooming in and out, the heatpoints get smaller and bigger during zooming. I see another user posted something similar on a lower version for pure iOS, perhaps it has never been fixed. (zooming video attached) https://github.com/maplibre/maplibre-native/issues/3168
Platform information (please complete the following information):
- Operating System: MacOS Sequoia 15.5
- Platform: React native
- React native: 0.81.4
- expo SDK 54
- MapLibre Version: v10.2.1 and v11.0.0.alpha5
Additional context
Map style used "https://tiles.openfreemap.org/styles/liberty"
map setup in react native:
import React, { useMemo, useRef, useEffect } from "react";
import { StyleSheet, View } from "react-native";
import {
Camera,
HeatmapLayer,
Images,
MapView,
OnPressEvent,
ShapeSource,
SymbolLayer,
type CameraRef,
} from "@maplibre/maplibre-react-native";
import type { FeatureCollection, Point } from "geojson";
import type { Coordinates, HeatmapPoint } from "../types";
import type { GymVenue } from "types/firestoreStructure/gyms";
export type MapLibreMapProps = {
center: Coordinates;
gyms: GymVenue[];
heat: HeatmapPoint[];
onSelectGym: (gym: GymVenue) => void;
};
// Free styles with labels (no key)
const DEFAULT_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
const mapStyleVars = {
gymIconSize: 0.08,
myPinSize: 0.8,
heatmap: { radius: 20, opacity: 0.7, intensity: 0.6 } as const,
} as const;
const MapLibreMap: React.FC<MapLibreMapProps> = ({ center, gyms, heat, onSelectGym }) => {
const cameraRef = useRef<CameraRef>(null);
useEffect(() => {
cameraRef.current?.setCamera({
centerCoordinate: [center.longitude, center.latitude],
zoomLevel: 11,
animationDuration: 600,
});
}, [center]);
const gymFeatures = useMemo<FeatureCollection<Point, { venueId: string; name: string }>>(
() => ({
type: "FeatureCollection",
features: gyms
.filter((g) => !!g.venueLocation?.coordinates)
.map((g) => ({
type: "Feature",
id: g.venueId,
properties: {
venueId: g.venueId,
name: g.venueName || "",
},
geometry: {
type: "Point",
coordinates: [
g.venueLocation!.coordinates!.longitude,
g.venueLocation!.coordinates!.latitude,
],
},
})),
}),
[gyms]
);
const myLocationFeature = useMemo<FeatureCollection<Point>>(
() => ({
type: "FeatureCollection",
features: [
{
type: "Feature",
id: "me",
properties: {},
geometry: { type: "Point", coordinates: [center.longitude, center.latitude] },
},
],
}),
[center.longitude, center.latitude]
);
const heatFeatures = useMemo<FeatureCollection<Point, { weight: number }>>(
() => ({
type: "FeatureCollection",
features: heat.map((p, idx) => ({
type: "Feature",
id: `h-${idx}`,
properties: { weight: p.weight ?? 1 },
geometry: { type: "Point", coordinates: [p.longitude, p.latitude] },
})),
}),
[heat]
);
const handleGymPress = (event: OnPressEvent) => {
const features = event?.features[0];
if (!features) return;
console.log(features)
if(features?.properties?.["cluster"]){
const numberOfItemsHere = features?.properties?.["point_count"];
alert(`There are ${numberOfItemsHere} gyms in this area. Zoom in to view more.`)
return;
}
const venueId: string | undefined = features?.properties?.["venueId"];
if (!venueId) return;
const gym = gyms.find((g) => g.venueId === venueId);
if (gym) onSelectGym(gym);
};
return (
<View style={styles.container}>
<MapView style={styles.map} logoEnabled={false} mapStyle={DEFAULT_STYLE_URL}>
<Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: [center.longitude, center.latitude],
zoomLevel: 11,
}}
/>
<Images images={{
gym: require("assets/images/gym-icon.png"),
me: require("assets/images/lpin.png")
}} />
{/* HEATMAP FIRST (underneath) */}
{heat.length > 0 && (
<ShapeSource id="heat" shape={heatFeatures}>
<HeatmapLayer
id="heatLayer"
style={{
heatmapWeight: ["get", "weight"],
heatmapOpacity: mapStyleVars.heatmap.opacity,
heatmapRadius: mapStyleVars.heatmap.radius,
heatmapIntensity: mapStyleVars.heatmap.intensity,
heatmapColor: [
"interpolate",
["linear"],
["heatmap-density"],
0, "rgba(0, 255, 0, 0)",
0.5, "rgba(0, 255, 0, 0.8)",
1, "rgba(255, 0, 0, 0.8)",
],
}}
/>
</ShapeSource>
)}
{/* GYMS ABOVE */}
<ShapeSource
id="gyms"
shape={gymFeatures}
onPress={handleGymPress}
cluster={true}
>
<SymbolLayer
id="gymSymbols"
// If TS complains about aboveLayerID in your version, remove this line;
// render order already keeps symbols above.
aboveLayerID="heatLayer"
style={{
iconImage: "gym",
iconSize: mapStyleVars.gymIconSize,
iconAllowOverlap: false,
iconIgnorePlacement: false,
iconPadding: [0],
}}
/>
</ShapeSource>
{/* Custom "my location" pin (render last so it's on top) */}
<ShapeSource id="me" shape={myLocationFeature}>
<SymbolLayer
aboveLayerID="gymSymbols"
id="mePin"
style={
{
iconImage: "me",
iconSize: mapStyleVars.myPinSize,
iconAllowOverlap: true,
iconIgnorePlacement: true,
}
}
/>
</ShapeSource>
</MapView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
flex: 1,
},
});
export default MapLibreMap;
@maplibre/maplibre-react-native Version
10.2.1
Which platforms does this occur on?
iOS Simulator
Which frameworks does this occur on?
Expo
Which architectures does this occur on?
New Architecture
Environment
expo-env-info 2.0.7 environment info:
System:
OS: macOS 15.5
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.19.4 - ~/.nvm/versions/node/v20.19.4/bin/node
Yarn: 1.22.22 - /opt/homebrew/bin/yarn
npm: 11.6.0 - ~/.nvm/versions/node/v20.19.4/bin/npm
Watchman: 2025.06.23.00 - /opt/homebrew/bin/watchman
Managers:
CocoaPods: 1.16.2 - /opt/homebrew/lib/ruby/gems/3.4.0/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 24.5, iOS 18.5, macOS 15.5, tvOS 18.5, visionOS 2.5, watchOS 11.5
IDEs:
Android Studio: 2024.2 AI-242.23726.103.2422.13103373
Xcode: 16.4/16F6 - /usr/bin/xcodebuild
npmGlobalPackages:
eas-cli: 16.19.3
Expo Workflow: managed
Thanks for the report, I think most if not all issues are upstream. Did you try setting different Radius on iOS and Android? I don't know if this should be exactly the same on both platforms, maybe there is some issue around pixel density.