Map: render meters as a GL CircleLayer instead of MarkerView
MarkerView (React overlays repositioned per frame) caused the pins to float and the camera to snap back to the user on pan/zoom/tap. Meters are now a ShapeSource + CircleLayer (GPU-drawn, coordinate-anchored); tapping a circle opens its parking flow via the source's native onPress. No React overlays = no floating, no camera thrash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0c343ab20
commit
3837200d89
1 changed files with 48 additions and 29 deletions
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Constants from 'expo-constants';
|
||||
import {
|
||||
MapView,
|
||||
Camera,
|
||||
MarkerView,
|
||||
ShapeSource,
|
||||
CircleLayer,
|
||||
UserLocation,
|
||||
} from '@maplibre/maplibre-react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -148,6 +148,31 @@ export function MapScreen() {
|
|||
await searchAt(last, 'last location');
|
||||
};
|
||||
|
||||
// Meters as a GeoJSON layer (GPU-drawn, coordinate-anchored) — far more stable
|
||||
// than React MarkerViews, which floated and thrashed the camera.
|
||||
const meterFeatures = useMemo(
|
||||
() => ({
|
||||
type: 'FeatureCollection' as const,
|
||||
features: zones.map((z, i) => ({
|
||||
type: 'Feature' as const,
|
||||
id: String(z.ZoneId ?? z.ScannerCode ?? i),
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [z.Long as number, z.Lat as number],
|
||||
},
|
||||
properties: { index: i, color: normalizeColor(z.BackgroundColor) ?? '#1e6f5c' },
|
||||
})),
|
||||
}),
|
||||
[zones],
|
||||
);
|
||||
|
||||
const onPinPress = (e: any) => {
|
||||
const idx = e?.features?.[0]?.properties?.index;
|
||||
if (typeof idx === 'number' && zones[idx]) {
|
||||
navigation.navigate('MeterDetail', { zone: zones[idx] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MapView
|
||||
|
|
@ -177,26 +202,26 @@ export function MapScreen() {
|
|||
}}
|
||||
/>
|
||||
|
||||
{zones.map((z) => (
|
||||
<MarkerView
|
||||
key={String(z.ZoneId ?? z.ScannerCode ?? z.TerminalSerNo)}
|
||||
coordinate={[z.Long as number, z.Lat as number]}
|
||||
anchor={{ x: 0.5, y: 1 }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('MeterDetail', { zone: z })}
|
||||
activeOpacity={0.7}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<Ionicons
|
||||
name="location-sharp"
|
||||
size={34}
|
||||
color={normalizeColor(z.BackgroundColor) ?? '#1e6f5c'}
|
||||
style={styles.pinShadow}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</MarkerView>
|
||||
))}
|
||||
<ShapeSource id="meters" shape={meterFeatures} onPress={onPinPress}>
|
||||
<CircleLayer
|
||||
id="meter-circles"
|
||||
style={{
|
||||
circleColor: ['get', 'color'],
|
||||
circleStrokeColor: '#ffffff',
|
||||
circleStrokeWidth: 2,
|
||||
circlePitchAlignment: 'map',
|
||||
circleRadius: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
10,
|
||||
5,
|
||||
16,
|
||||
9,
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</ShapeSource>
|
||||
</MapView>
|
||||
|
||||
<View style={[styles.statusBar, { top: insets.top + 12 }]}>
|
||||
|
|
@ -224,12 +249,6 @@ export function MapScreen() {
|
|||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
map: { flex: 1 },
|
||||
// White halo so the colored pin stays visible on both light and dark tiles.
|
||||
pinShadow: {
|
||||
textShadowColor: 'rgba(255,255,255,0.9)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 3,
|
||||
},
|
||||
statusBar: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue