@chauffleet/expo-custom-map
Version:
Open source custom map library for Expo/React Native. Use your own tiles without Google Maps, Mapbox, or API keys. Created by ChaufFleet.
212 lines • 10.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomMapView = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const react_native_1 = require("react-native");
const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
const react_native_gesture_handler_1 = require("react-native-gesture-handler");
const TileLayer_1 = require("./components/TileLayer");
const { width: screenWidth, height: screenHeight } = react_native_1.Dimensions.get('window');
// Constantes optimisées pour la fluidité parfaite Google Maps
const MIN_ZOOM = 1;
const MAX_ZOOM = 18;
const DECAY_RATE = 0.998;
const VELOCITY_SCALE = 0.8;
/**
* CustomMapView - Version ultra-optimisée pour une fluidité Google Maps
*
* Principes clés :
* 1. Toutes les animations restent sur l'UI thread
* 2. Minimal runOnJS() calls
* 3. Séparation claire entre gesture transforms et region state
* 4. TileLayer optimisé avec memoization agressive
*/
const CustomMapView = ({ initialRegion = {
latitude: 48.8566,
longitude: 2.3522,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}, onRegionChange, onRegionChangeComplete, style, tileUrlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', children, }) => {
// Région de base - change uniquement quand les gestes se terminent
const baseRegion = (0, react_1.useRef)(initialRegion);
const isGesturing = (0, react_1.useRef)(false);
// Valeurs animées - PURE UI THREAD
const translateX = (0, react_native_reanimated_1.useSharedValue)(0);
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
const scale = (0, react_native_reanimated_1.useSharedValue)(1);
// Point focal pour le zoom (centre de l'écran par défaut)
const focalX = (0, react_native_reanimated_1.useSharedValue)(screenWidth / 2);
const focalY = (0, react_native_reanimated_1.useSharedValue)(screenHeight / 2);
// Fonction de conversion géographique optimisée (côté JS)
const calculateRegionFromTransforms = (0, react_1.useCallback)((deltaX, deltaY, scaleChange, centerX = screenWidth / 2, centerY = screenHeight / 2) => {
const base = baseRegion.current;
// Calcul du déplacement en degrés
const pixelsPerDegreeLat = screenHeight / base.latitudeDelta;
const pixelsPerDegreeLon = screenWidth / base.longitudeDelta;
const latChange = -deltaY / pixelsPerDegreeLat;
const lonChange = -deltaX / pixelsPerDegreeLon;
// Nouveau centre après déplacement
let newLat = base.latitude + latChange;
let newLon = base.longitude + lonChange;
// Calcul du nouveau zoom
const newLatDelta = base.latitudeDelta / scaleChange;
const newLonDelta = base.longitudeDelta / scaleChange;
// Ajustement du centre pour le point focal du zoom
if (scaleChange !== 1) {
const focalOffsetX = centerX - screenWidth / 2;
const focalOffsetY = centerY - screenHeight / 2;
const focalLatOffset = -focalOffsetY / pixelsPerDegreeLat;
const focalLonOffset = -focalOffsetX / pixelsPerDegreeLon;
newLat += focalLatOffset * (1 - 1 / scaleChange);
newLon += focalLonOffset * (1 - 1 / scaleChange);
}
// Contraintes
newLat = Math.max(-85, Math.min(85, newLat));
const maxDelta = 180;
const minDelta = 0.0001;
return {
latitude: newLat,
longitude: newLon,
latitudeDelta: Math.max(minDelta, Math.min(maxDelta, newLatDelta)),
longitudeDelta: Math.max(minDelta, Math.min(maxDelta, newLonDelta)),
};
}, []);
// Mise à jour région - uniquement quand nécessaire
const updateRegionState = (0, react_1.useCallback)((newRegion) => {
baseRegion.current = newRegion;
onRegionChange?.(newRegion);
}, [onRegionChange]);
const finalizeRegion = (0, react_1.useCallback)((finalRegion) => {
baseRegion.current = finalRegion;
onRegionChangeComplete?.(finalRegion);
}, [onRegionChangeComplete]);
// Geste Pan - Ultra optimisé
const panGesture = react_native_gesture_handler_1.Gesture.Pan()
.minDistance(1)
.onBegin(() => {
isGesturing.current = true;
(0, react_native_reanimated_1.cancelAnimation)(translateX);
(0, react_native_reanimated_1.cancelAnimation)(translateY);
})
.onChange((event) => {
// PURE UI THREAD - Aucun runOnJS ici !
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd((event) => {
const hasVelocity = Math.abs(event.velocityX) > 300 || Math.abs(event.velocityY) > 300;
if (hasVelocity) {
// Inertie fluide
translateX.value = (0, react_native_reanimated_1.withDecay)({
velocity: event.velocityX * VELOCITY_SCALE,
deceleration: DECAY_RATE,
rubberBandEffect: true,
clamp: [-screenWidth * 3, screenWidth * 3],
});
translateY.value = (0, react_native_reanimated_1.withDecay)({
velocity: event.velocityY * VELOCITY_SCALE,
deceleration: DECAY_RATE,
rubberBandEffect: true,
clamp: [-screenHeight * 3, screenHeight * 3],
}, (finished) => {
if (finished) {
// Une seule fois à la fin
(0, react_native_reanimated_1.runOnJS)(() => {
const finalRegion = calculateRegionFromTransforms(translateX.value, translateY.value, scale.value);
// Reset des transforms
translateX.value = 0;
translateY.value = 0;
scale.value = 1;
isGesturing.current = false;
finalizeRegion(finalRegion);
})();
}
});
}
else {
// Pas d'inertie - finalisation immédiate
(0, react_native_reanimated_1.runOnJS)(() => {
const finalRegion = calculateRegionFromTransforms(translateX.value, translateY.value, scale.value);
translateX.value = 0;
translateY.value = 0;
scale.value = 1;
isGesturing.current = false;
finalizeRegion(finalRegion);
})();
}
});
// Geste Pinch - Ultra optimisé avec focal point
const pinchGesture = react_native_gesture_handler_1.Gesture.Pinch()
.onBegin(() => {
isGesturing.current = true;
(0, react_native_reanimated_1.cancelAnimation)(scale);
})
.onUpdate((event) => {
// PURE UI THREAD
scale.value = Math.max(0.5, Math.min(4, event.scale));
focalX.value = event.focalX;
focalY.value = event.focalY;
})
.onEnd(() => {
(0, react_native_reanimated_1.runOnJS)(() => {
const finalRegion = calculateRegionFromTransforms(translateX.value, translateY.value, scale.value, focalX.value, focalY.value);
translateX.value = 0;
translateY.value = 0;
scale.value = 1;
focalX.value = screenWidth / 2;
focalY.value = screenHeight / 2;
isGesturing.current = false;
finalizeRegion(finalRegion);
})();
});
// Gestes simultanés
const composedGesture = react_native_gesture_handler_1.Gesture.Simultaneous(panGesture, pinchGesture);
// Style animé - PURE UI THREAD
const animatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
};
}, []);
return ((0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureHandlerRootView, { style: [{ flex: 1 }, style], children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 1, overflow: 'hidden' }, children: (0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureDetector, { gesture: composedGesture, children: (0, jsx_runtime_1.jsxs)(react_native_reanimated_1.default.View, { style: [{ flex: 1 }, animatedStyle], children: [(0, jsx_runtime_1.jsx)(TileLayer_1.TileLayer, { region: baseRegion.current, tileUrlTemplate: tileUrlTemplate, screenWidth: screenWidth, screenHeight: screenHeight, isGesturing: isGesturing.current }), children] }) }) }) }));
};
exports.CustomMapView = CustomMapView;
exports.default = exports.CustomMapView;
//# sourceMappingURL=CustomMapView.js.map