mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 16:51:07 -05:00
881 lines
27 KiB
JavaScript
881 lines
27 KiB
JavaScript
/**
|
|
* ThrillWiki Mobile Touch Support - Enhanced Mobile and Touch Experience
|
|
*
|
|
* This module provides mobile-optimized interactions, touch-friendly controls,
|
|
* responsive map sizing, and battery-conscious features for mobile devices
|
|
*/
|
|
|
|
class MobileTouchSupport {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
enableTouchOptimizations: true,
|
|
enableSwipeGestures: true,
|
|
enablePinchZoom: true,
|
|
enableResponsiveResize: true,
|
|
enableBatteryOptimization: true,
|
|
touchDebounceDelay: 150,
|
|
swipeThreshold: 50,
|
|
swipeVelocityThreshold: 0.3,
|
|
maxTouchPoints: 2,
|
|
orientationChangeDelay: 300,
|
|
...options
|
|
};
|
|
|
|
this.isMobile = this.detectMobileDevice();
|
|
this.isTouch = this.detectTouchSupport();
|
|
this.orientation = this.getOrientation();
|
|
this.mapInstances = new Set();
|
|
this.touchHandlers = new Map();
|
|
this.gestureState = {
|
|
isActive: false,
|
|
startDistance: 0,
|
|
startCenter: null,
|
|
lastTouchTime: 0
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize mobile touch support
|
|
*/
|
|
init() {
|
|
if (!this.isTouch && !this.isMobile) {
|
|
console.log('Mobile touch support not needed for this device');
|
|
return;
|
|
}
|
|
|
|
this.setupTouchOptimizations();
|
|
this.setupSwipeGestures();
|
|
this.setupResponsiveHandling();
|
|
this.setupBatteryOptimization();
|
|
this.setupAccessibilityEnhancements();
|
|
this.bindEventHandlers();
|
|
|
|
console.log('Mobile touch support initialized');
|
|
}
|
|
|
|
/**
|
|
* Detect if device is mobile
|
|
*/
|
|
detectMobileDevice() {
|
|
const userAgent = navigator.userAgent.toLowerCase();
|
|
const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
|
|
|
|
return mobileKeywords.some(keyword => userAgent.includes(keyword)) ||
|
|
window.innerWidth <= 768 ||
|
|
(typeof window.orientation !== 'undefined');
|
|
}
|
|
|
|
/**
|
|
* Detect touch support
|
|
*/
|
|
detectTouchSupport() {
|
|
return 'ontouchstart' in window ||
|
|
navigator.maxTouchPoints > 0 ||
|
|
navigator.msMaxTouchPoints > 0;
|
|
}
|
|
|
|
/**
|
|
* Get current orientation
|
|
*/
|
|
getOrientation() {
|
|
if (screen.orientation) {
|
|
return screen.orientation.angle;
|
|
} else if (window.orientation !== undefined) {
|
|
return window.orientation;
|
|
}
|
|
return window.innerWidth > window.innerHeight ? 90 : 0;
|
|
}
|
|
|
|
/**
|
|
* Setup touch optimizations
|
|
*/
|
|
setupTouchOptimizations() {
|
|
if (!this.options.enableTouchOptimizations) return;
|
|
|
|
// Add touch-optimized styles
|
|
this.addTouchStyles();
|
|
|
|
// Enhance touch targets
|
|
this.enhanceTouchTargets();
|
|
|
|
// Optimize scroll behavior
|
|
this.optimizeScrollBehavior();
|
|
|
|
// Setup touch feedback
|
|
this.setupTouchFeedback();
|
|
}
|
|
|
|
/**
|
|
* Add touch-optimized CSS styles
|
|
*/
|
|
addTouchStyles() {
|
|
if (document.getElementById('mobile-touch-styles')) return;
|
|
|
|
const styles = `
|
|
<style id="mobile-touch-styles">
|
|
@media (max-width: 768px) {
|
|
/* Touch-friendly button sizes */
|
|
.btn, button, .filter-chip, .filter-pill {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
padding: 12px 16px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Larger touch targets for map controls */
|
|
.leaflet-control-zoom a {
|
|
width: 44px !important;
|
|
height: 44px !important;
|
|
line-height: 44px !important;
|
|
font-size: 18px !important;
|
|
}
|
|
|
|
/* Mobile-optimized map containers */
|
|
.map-container {
|
|
height: 60vh !important;
|
|
min-height: 300px !important;
|
|
}
|
|
|
|
/* Touch-friendly popup styling */
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
margin: 16px 20px;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Improved form controls */
|
|
input, select, textarea {
|
|
font-size: 16px !important; /* Prevents zoom on iOS */
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Touch-friendly filter panels */
|
|
.filter-panel {
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
/* Mobile navigation improvements */
|
|
.roadtrip-planner {
|
|
padding: 16px;
|
|
}
|
|
|
|
.parks-list .park-item {
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
border-radius: 12px;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
/* Swipe indicators */
|
|
.swipe-indicator {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 4px;
|
|
height: 40px;
|
|
background: rgba(59, 130, 246, 0.5);
|
|
border-radius: 2px;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.swipe-indicator.left {
|
|
left: 8px;
|
|
}
|
|
|
|
.swipe-indicator.right {
|
|
right: 8px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
/* Extra small screens */
|
|
.map-container {
|
|
height: 50vh !important;
|
|
}
|
|
|
|
.filter-panel {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1000;
|
|
max-height: 50vh;
|
|
overflow-y: auto;
|
|
border-radius: 16px 16px 0 0;
|
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
|
}
|
|
}
|
|
|
|
/* Touch feedback */
|
|
.touch-feedback {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.touch-feedback::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 0;
|
|
height: 0;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
transform: translate(-50%, -50%);
|
|
transition: width 0.3s ease, height 0.3s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.touch-feedback.active::after {
|
|
width: 100px;
|
|
height: 100px;
|
|
}
|
|
|
|
/* Prevent text selection on mobile */
|
|
.no-select {
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
-khtml-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
/* Optimize touch scrolling */
|
|
.touch-scroll {
|
|
-webkit-overflow-scrolling: touch;
|
|
overflow-scrolling: touch;
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
document.head.insertAdjacentHTML('beforeend', styles);
|
|
}
|
|
|
|
/**
|
|
* Enhance touch targets for better accessibility
|
|
*/
|
|
enhanceTouchTargets() {
|
|
const smallTargets = document.querySelectorAll('button, .btn, a, input[type="checkbox"], input[type="radio"]');
|
|
|
|
smallTargets.forEach(target => {
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
// If target is smaller than 44px (Apple's recommended minimum), enhance it
|
|
if (rect.width < 44 || rect.height < 44) {
|
|
target.classList.add('touch-enhanced');
|
|
target.style.minWidth = '44px';
|
|
target.style.minHeight = '44px';
|
|
target.style.display = 'inline-flex';
|
|
target.style.alignItems = 'center';
|
|
target.style.justifyContent = 'center';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Optimize scroll behavior for mobile
|
|
*/
|
|
optimizeScrollBehavior() {
|
|
// Add momentum scrolling to scrollable elements
|
|
const scrollableElements = document.querySelectorAll('.scrollable, .overflow-auto, .overflow-y-auto');
|
|
|
|
scrollableElements.forEach(element => {
|
|
element.classList.add('touch-scroll');
|
|
element.style.webkitOverflowScrolling = 'touch';
|
|
});
|
|
|
|
// Prevent body scroll when interacting with maps
|
|
document.addEventListener('touchstart', (e) => {
|
|
if (e.target.closest('.leaflet-container')) {
|
|
e.preventDefault();
|
|
}
|
|
}, { passive: false });
|
|
}
|
|
|
|
/**
|
|
* Setup touch feedback for interactive elements
|
|
*/
|
|
setupTouchFeedback() {
|
|
const interactiveElements = document.querySelectorAll('button, .btn, .filter-chip, .filter-pill, .park-item');
|
|
|
|
interactiveElements.forEach(element => {
|
|
element.classList.add('touch-feedback');
|
|
|
|
element.addEventListener('touchstart', (e) => {
|
|
element.classList.add('active');
|
|
|
|
setTimeout(() => {
|
|
element.classList.remove('active');
|
|
}, 300);
|
|
}, { passive: true });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup swipe gesture support
|
|
*/
|
|
setupSwipeGestures() {
|
|
if (!this.options.enableSwipeGestures) return;
|
|
|
|
let touchStartX = 0;
|
|
let touchStartY = 0;
|
|
let touchStartTime = 0;
|
|
|
|
document.addEventListener('touchstart', (e) => {
|
|
if (e.touches.length === 1) {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchStartY = e.touches[0].clientY;
|
|
touchStartTime = Date.now();
|
|
}
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchend', (e) => {
|
|
if (e.changedTouches.length === 1) {
|
|
const touchEndX = e.changedTouches[0].clientX;
|
|
const touchEndY = e.changedTouches[0].clientY;
|
|
const touchEndTime = Date.now();
|
|
|
|
const deltaX = touchEndX - touchStartX;
|
|
const deltaY = touchEndY - touchStartY;
|
|
const deltaTime = touchEndTime - touchStartTime;
|
|
const velocity = Math.abs(deltaX) / deltaTime;
|
|
|
|
// Check if this is a swipe gesture
|
|
if (Math.abs(deltaX) > this.options.swipeThreshold &&
|
|
Math.abs(deltaY) < Math.abs(deltaX) &&
|
|
velocity > this.options.swipeVelocityThreshold) {
|
|
|
|
const direction = deltaX > 0 ? 'right' : 'left';
|
|
this.handleSwipeGesture(direction, e.target);
|
|
}
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle swipe gestures
|
|
*/
|
|
handleSwipeGesture(direction, target) {
|
|
// Handle swipe on filter panels
|
|
if (target.closest('.filter-panel')) {
|
|
if (direction === 'down' || direction === 'up') {
|
|
this.toggleFilterPanel();
|
|
}
|
|
}
|
|
|
|
// Handle swipe on road trip list
|
|
if (target.closest('.parks-list')) {
|
|
if (direction === 'left') {
|
|
this.showParkActions(target);
|
|
} else if (direction === 'right') {
|
|
this.hideParkActions(target);
|
|
}
|
|
}
|
|
|
|
// Emit custom swipe event
|
|
const swipeEvent = new CustomEvent('swipe', {
|
|
detail: { direction, target }
|
|
});
|
|
document.dispatchEvent(swipeEvent);
|
|
}
|
|
|
|
/**
|
|
* Setup responsive handling for orientation changes
|
|
*/
|
|
setupResponsiveHandling() {
|
|
if (!this.options.enableResponsiveResize) return;
|
|
|
|
// Handle orientation changes
|
|
window.addEventListener('orientationchange', () => {
|
|
setTimeout(() => {
|
|
this.handleOrientationChange();
|
|
}, this.options.orientationChangeDelay);
|
|
});
|
|
|
|
// Handle window resize
|
|
let resizeTimeout;
|
|
window.addEventListener('resize', () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
this.handleWindowResize();
|
|
}, 250);
|
|
});
|
|
|
|
// Handle viewport changes (for mobile browsers with dynamic toolbars)
|
|
this.setupViewportHandler();
|
|
}
|
|
|
|
/**
|
|
* Handle orientation change
|
|
*/
|
|
handleOrientationChange() {
|
|
const newOrientation = this.getOrientation();
|
|
|
|
if (newOrientation !== this.orientation) {
|
|
this.orientation = newOrientation;
|
|
|
|
// Resize all map instances
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.invalidateSize) {
|
|
mapInstance.invalidateSize();
|
|
}
|
|
});
|
|
|
|
// Emit orientation change event
|
|
const orientationEvent = new CustomEvent('orientationChanged', {
|
|
detail: { orientation: this.orientation }
|
|
});
|
|
document.dispatchEvent(orientationEvent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle window resize
|
|
*/
|
|
handleWindowResize() {
|
|
// Update mobile detection
|
|
this.isMobile = this.detectMobileDevice();
|
|
|
|
// Resize map instances
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.invalidateSize) {
|
|
mapInstance.invalidateSize();
|
|
}
|
|
});
|
|
|
|
// Update touch targets
|
|
this.enhanceTouchTargets();
|
|
}
|
|
|
|
/**
|
|
* Setup viewport handler for dynamic mobile toolbars
|
|
*/
|
|
setupViewportHandler() {
|
|
// Use visual viewport API if available
|
|
if (window.visualViewport) {
|
|
window.visualViewport.addEventListener('resize', () => {
|
|
this.handleViewportChange();
|
|
});
|
|
}
|
|
|
|
// Fallback for older browsers
|
|
let lastHeight = window.innerHeight;
|
|
|
|
const checkViewportChange = () => {
|
|
if (Math.abs(window.innerHeight - lastHeight) > 100) {
|
|
lastHeight = window.innerHeight;
|
|
this.handleViewportChange();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('resize', checkViewportChange);
|
|
document.addEventListener('focusin', checkViewportChange);
|
|
document.addEventListener('focusout', checkViewportChange);
|
|
}
|
|
|
|
/**
|
|
* Handle viewport changes
|
|
*/
|
|
handleViewportChange() {
|
|
// Adjust map container heights
|
|
const mapContainers = document.querySelectorAll('.map-container');
|
|
mapContainers.forEach(container => {
|
|
const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
|
|
|
if (viewportHeight < 500) {
|
|
container.style.height = '40vh';
|
|
} else {
|
|
container.style.height = ''; // Reset to CSS default
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup battery optimization
|
|
*/
|
|
setupBatteryOptimization() {
|
|
if (!this.options.enableBatteryOptimization) return;
|
|
|
|
// Reduce update frequency when battery is low
|
|
if ('getBattery' in navigator) {
|
|
navigator.getBattery().then(battery => {
|
|
const optimizeBattery = () => {
|
|
if (battery.level < 0.2) { // Battery below 20%
|
|
this.enableBatterySaveMode();
|
|
} else {
|
|
this.disableBatterySaveMode();
|
|
}
|
|
};
|
|
|
|
battery.addEventListener('levelchange', optimizeBattery);
|
|
optimizeBattery();
|
|
});
|
|
}
|
|
|
|
// Reduce activity when page is not visible
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
this.pauseNonEssentialFeatures();
|
|
} else {
|
|
this.resumeNonEssentialFeatures();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enable battery save mode
|
|
*/
|
|
enableBatterySaveMode() {
|
|
console.log('Enabling battery save mode');
|
|
|
|
// Reduce map update frequency
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.options) {
|
|
mapInstance.options.updateInterval = 5000; // Increase to 5 seconds
|
|
}
|
|
});
|
|
|
|
// Disable animations
|
|
document.body.classList.add('battery-save-mode');
|
|
}
|
|
|
|
/**
|
|
* Disable battery save mode
|
|
*/
|
|
disableBatterySaveMode() {
|
|
console.log('Disabling battery save mode');
|
|
|
|
// Restore normal update frequency
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.options) {
|
|
mapInstance.options.updateInterval = 1000; // Restore to 1 second
|
|
}
|
|
});
|
|
|
|
// Re-enable animations
|
|
document.body.classList.remove('battery-save-mode');
|
|
}
|
|
|
|
/**
|
|
* Pause non-essential features
|
|
*/
|
|
pauseNonEssentialFeatures() {
|
|
// Pause location watching
|
|
if (window.userLocation && window.userLocation.stopWatching) {
|
|
window.userLocation.stopWatching();
|
|
}
|
|
|
|
// Reduce map updates
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.pauseUpdates) {
|
|
mapInstance.pauseUpdates();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resume non-essential features
|
|
*/
|
|
resumeNonEssentialFeatures() {
|
|
// Resume location watching if it was active
|
|
if (window.userLocation && window.userLocation.options.watchPosition) {
|
|
window.userLocation.startWatching();
|
|
}
|
|
|
|
// Resume map updates
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.resumeUpdates) {
|
|
mapInstance.resumeUpdates();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup accessibility enhancements for mobile
|
|
*/
|
|
setupAccessibilityEnhancements() {
|
|
// Add focus indicators for touch navigation
|
|
const focusableElements = document.querySelectorAll('button, a, input, select, textarea, [tabindex]');
|
|
|
|
focusableElements.forEach(element => {
|
|
element.addEventListener('focus', () => {
|
|
element.classList.add('touch-focused');
|
|
});
|
|
|
|
element.addEventListener('blur', () => {
|
|
element.classList.remove('touch-focused');
|
|
});
|
|
});
|
|
|
|
// Enhance keyboard navigation
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Tab') {
|
|
document.body.classList.add('keyboard-navigation');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('mousedown', () => {
|
|
document.body.classList.remove('keyboard-navigation');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEventHandlers() {
|
|
// Handle double-tap to zoom
|
|
this.setupDoubleTapZoom();
|
|
|
|
// Handle long press
|
|
this.setupLongPress();
|
|
|
|
// Handle pinch gestures
|
|
if (this.options.enablePinchZoom) {
|
|
this.setupPinchZoom();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup double-tap to zoom
|
|
*/
|
|
setupDoubleTapZoom() {
|
|
let lastTapTime = 0;
|
|
|
|
document.addEventListener('touchend', (e) => {
|
|
const currentTime = Date.now();
|
|
|
|
if (currentTime - lastTapTime < 300) {
|
|
// Double tap detected
|
|
const target = e.target;
|
|
if (target.closest('.leaflet-container')) {
|
|
this.handleDoubleTapZoom(e);
|
|
}
|
|
}
|
|
|
|
lastTapTime = currentTime;
|
|
}, { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle double-tap zoom
|
|
*/
|
|
handleDoubleTapZoom(e) {
|
|
const mapContainer = e.target.closest('.leaflet-container');
|
|
if (!mapContainer) return;
|
|
|
|
// Find associated map instance
|
|
this.mapInstances.forEach(mapInstance => {
|
|
if (mapInstance.getContainer() === mapContainer) {
|
|
const currentZoom = mapInstance.getZoom();
|
|
const newZoom = currentZoom < mapInstance.getMaxZoom() ? currentZoom + 2 : mapInstance.getMinZoom();
|
|
|
|
mapInstance.setZoom(newZoom, {
|
|
animate: true,
|
|
duration: 0.3
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup long press detection
|
|
*/
|
|
setupLongPress() {
|
|
let pressTimer;
|
|
|
|
document.addEventListener('touchstart', (e) => {
|
|
pressTimer = setTimeout(() => {
|
|
this.handleLongPress(e);
|
|
}, 750); // 750ms for long press
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchend', () => {
|
|
clearTimeout(pressTimer);
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchmove', () => {
|
|
clearTimeout(pressTimer);
|
|
}, { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle long press
|
|
*/
|
|
handleLongPress(e) {
|
|
const target = e.target;
|
|
|
|
// Emit long press event
|
|
const longPressEvent = new CustomEvent('longPress', {
|
|
detail: { target, touches: e.touches }
|
|
});
|
|
target.dispatchEvent(longPressEvent);
|
|
|
|
// Provide haptic feedback if available
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(50);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup pinch zoom for maps
|
|
*/
|
|
setupPinchZoom() {
|
|
document.addEventListener('touchstart', (e) => {
|
|
if (e.touches.length === 2) {
|
|
this.gestureState.isActive = true;
|
|
this.gestureState.startDistance = this.getDistance(e.touches[0], e.touches[1]);
|
|
this.gestureState.startCenter = this.getCenter(e.touches[0], e.touches[1]);
|
|
}
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchmove', (e) => {
|
|
if (this.gestureState.isActive && e.touches.length === 2) {
|
|
this.handlePinchZoom(e);
|
|
}
|
|
}, { passive: false });
|
|
|
|
document.addEventListener('touchend', () => {
|
|
this.gestureState.isActive = false;
|
|
}, { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle pinch zoom gesture
|
|
*/
|
|
handlePinchZoom(e) {
|
|
if (!e.target.closest('.leaflet-container')) return;
|
|
|
|
const currentDistance = this.getDistance(e.touches[0], e.touches[1]);
|
|
const scale = currentDistance / this.gestureState.startDistance;
|
|
|
|
// Emit pinch event
|
|
const pinchEvent = new CustomEvent('pinch', {
|
|
detail: { scale, center: this.gestureState.startCenter }
|
|
});
|
|
e.target.dispatchEvent(pinchEvent);
|
|
}
|
|
|
|
/**
|
|
* Get distance between two touch points
|
|
*/
|
|
getDistance(touch1, touch2) {
|
|
const dx = touch1.clientX - touch2.clientX;
|
|
const dy = touch1.clientY - touch2.clientY;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
/**
|
|
* Get center point between two touches
|
|
*/
|
|
getCenter(touch1, touch2) {
|
|
return {
|
|
x: (touch1.clientX + touch2.clientX) / 2,
|
|
y: (touch1.clientY + touch2.clientY) / 2
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Register map instance for mobile optimizations
|
|
*/
|
|
registerMapInstance(mapInstance) {
|
|
this.mapInstances.add(mapInstance);
|
|
|
|
// Apply mobile-specific map options
|
|
if (this.isMobile && mapInstance.options) {
|
|
mapInstance.options.zoomControl = false; // Use custom larger controls
|
|
mapInstance.options.attributionControl = false; // Save space
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister map instance
|
|
*/
|
|
unregisterMapInstance(mapInstance) {
|
|
this.mapInstances.delete(mapInstance);
|
|
}
|
|
|
|
/**
|
|
* Toggle filter panel for mobile
|
|
*/
|
|
toggleFilterPanel() {
|
|
const filterPanel = document.querySelector('.filter-panel');
|
|
if (filterPanel) {
|
|
filterPanel.classList.toggle('mobile-expanded');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show park actions on swipe
|
|
*/
|
|
showParkActions(target) {
|
|
const parkItem = target.closest('.park-item');
|
|
if (parkItem) {
|
|
parkItem.classList.add('actions-visible');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide park actions
|
|
*/
|
|
hideParkActions(target) {
|
|
const parkItem = target.closest('.park-item');
|
|
if (parkItem) {
|
|
parkItem.classList.remove('actions-visible');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if device is mobile
|
|
*/
|
|
isMobileDevice() {
|
|
return this.isMobile;
|
|
}
|
|
|
|
/**
|
|
* Check if device supports touch
|
|
*/
|
|
isTouchDevice() {
|
|
return this.isTouch;
|
|
}
|
|
|
|
/**
|
|
* Get device info
|
|
*/
|
|
getDeviceInfo() {
|
|
return {
|
|
isMobile: this.isMobile,
|
|
isTouch: this.isTouch,
|
|
orientation: this.orientation,
|
|
viewportWidth: window.innerWidth,
|
|
viewportHeight: window.innerHeight,
|
|
pixelRatio: window.devicePixelRatio || 1
|
|
};
|
|
}
|
|
}
|
|
|
|
// Auto-initialize mobile touch support
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
window.mobileTouchSupport = new MobileTouchSupport();
|
|
|
|
// Register existing map instances
|
|
if (window.thrillwikiMap) {
|
|
window.mobileTouchSupport.registerMapInstance(window.thrillwikiMap);
|
|
}
|
|
});
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = MobileTouchSupport;
|
|
} else {
|
|
window.MobileTouchSupport = MobileTouchSupport;
|
|
} |