From 3837200d898dc38f3a171074f8c671b5c1298aa8 Mon Sep 17 00:00:00 2001 From: Hank Date: Mon, 6 Jul 2026 10:46:10 -0700 Subject: [PATCH] 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 --- app/src/screens/MapScreen.tsx | 77 ++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index be2b666..4252701 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -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 ( - {zones.map((z) => ( - - navigation.navigate('MeterDetail', { zone: z })} - activeOpacity={0.7} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} - > - - - - ))} + + + @@ -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,