-
Notifications
You must be signed in to change notification settings - Fork 8
Open
Description
Hello
Thanks for your valuable contributions to this repository,I'm currently encountering some issues with network requests in my application. I've been trying my best to avoid using third-party libraries, but I'm still experiencing failures. I was wondering if you could kindly provide some guidance or assistance.
demo
main.py
import sys
import math
import json
from pathlib import Path
from collections import deque, defaultdict
from datetime import datetime
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QSlider, QDialog,
QLineEdit, QCheckBox, QFormLayout, QDialogButtonBox,
QComboBox, QSpinBox, QGroupBox, QMessageBox, QGestureEvent,
QPinchGesture, QPanGesture, QGesture, QSplitter, QTextEdit,
QScrollArea)
from PySide6.QtCore import Qt, QPoint, QRect, QThread, Signal, QTimer, QSettings, QUrl, QByteArray, QPointF
from PySide6.QtGui import (QPainter, QPixmap, QWheelEvent, QMouseEvent, QAction, QPen,
QBrush, QPolygon, QColor, QIcon, QTouchEvent, QFont, QTransform)
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
def get_resource_path(*args):
"""Get resource file path"""
# If it's a packaged executable
base_path = Path(sys.executable).parent.joinpath(*args)
if base_path.exists():
print('prd')
return str(base_path)
else:
# If it's development environment
print('dev')
base_path = Path(__file__).parent.parent.joinpath(*args)
return str(base_path)
class LogWidget(QWidget):
"""Log display component"""
def __init__(self):
super().__init__()
self.setup_ui()
def setup_ui(self):
"""Setup UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5)
# Title
title_label = QLabel("Request Log")
title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #333; padding: 5px;")
layout.addWidget(title_label)
# Log text box
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
# Touch-friendly style
self.log_text.setStyleSheet("""
QTextEdit {
background-color: #1e1e1e;
color: #ffffff;
font-family: 'Roboto', sans-serif;
font-size: 12px;
border: 2px solid #555;
border-radius: 8px;
padding: 8px;
}
QTextEdit::corner {
background-color: #1e1e1e;
}
""")
# Enable touch scrolling
self.log_text.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.log_text.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
layout.addWidget(self.log_text)
# Control buttons
button_layout = QHBoxLayout()
self.clear_button = QPushButton("Clear Log")
self.clear_button.clicked.connect(self.clear_log)
self.clear_button.setMinimumHeight(44)
self.clear_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
padding: 8px 16px;
}
QPushButton:pressed {
background-color: #d32f2f;
}
""")
self.auto_scroll_checkbox = QCheckBox("Auto Scroll")
self.auto_scroll_checkbox.setChecked(True)
self.auto_scroll_checkbox.setMinimumHeight(44)
self.auto_scroll_checkbox.setStyleSheet("""
QCheckBox {
font-size: 14px;
color: #333;
padding: 8px;
}
QCheckBox::indicator {
width: 20px;
height: 20px;
}
""")
button_layout.addWidget(self.clear_button)
button_layout.addWidget(self.auto_scroll_checkbox)
button_layout.addStretch()
layout.addLayout(button_layout)
def add_log(self, message, level="INFO"):
"""Add log message"""
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
# Set color based on level
color_map = {
"INFO": "#ffffff",
"SUCCESS": "#4caf50",
"ERROR": "#f44336",
"WARNING": "#ff9800",
"DEBUG": "#2196f3",
"TOUCH": "#9c27b0"
}
color = color_map.get(level, "#ffffff")
# Add colored log
html_message = f'<span style="color: #888;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> <span style="color: {color};">{message}</span>'
self.log_text.append(html_message)
# Auto scroll to bottom
if self.auto_scroll_checkbox.isChecked():
scrollbar = self.log_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def clear_log(self):
"""Clear log"""
self.log_text.clear()
self.add_log("Log cleared", "INFO")
class TileCache:
"""Multi-level tile cache manager"""
def __init__(self, max_total_tiles=1000, max_tiles_per_zoom=300):
self.max_total_tiles = max_total_tiles
self.max_tiles_per_zoom = max_tiles_per_zoom
# Cache grouped by level {zoom_level: {tile_key: (pixmap, access_time)}}
self.cache_by_zoom = defaultdict(dict)
# Global access time counter
self.access_counter = 0
# Statistics
self.stats = {
"total_tiles": 0,
"zoom_levels": set(),
"cache_hits": 0,
"cache_misses": 0
}
def get_tile_key(self, x, y, z):
"""Generate tile key"""
return f"{x}_{y}"
def get(self, x, y, z):
"""Get tile"""
tile_key = self.get_tile_key(x, y, z)
zoom_cache = self.cache_by_zoom.get(z, {})
if tile_key in zoom_cache:
pixmap, _ = zoom_cache[tile_key]
# Update access time
self.access_counter += 1
zoom_cache[tile_key] = (pixmap, self.access_counter)
self.stats["cache_hits"] += 1
return pixmap
else:
self.stats["cache_misses"] += 1
return None
def put(self, x, y, z, pixmap):
"""Store tile"""
tile_key = self.get_tile_key(x, y, z)
self.access_counter += 1
# Ensure cache dict exists
if z not in self.cache_by_zoom:
self.cache_by_zoom[z] = {}
zoom_cache = self.cache_by_zoom[z]
# If this level's cache is full, remove oldest
if len(zoom_cache) >= self.max_tiles_per_zoom:
self._evict_oldest_in_zoom(z)
# Add new tile
zoom_cache[tile_key] = (pixmap, self.access_counter)
# Update statistics
self.stats["total_tiles"] = sum(len(cache) for cache in self.cache_by_zoom.values())
self.stats["zoom_levels"].add(z)
# Check total cache size
if self.stats["total_tiles"] > self.max_total_tiles:
self._evict_least_used_zoom()
def _evict_oldest_in_zoom(self, zoom_level):
"""Remove oldest tile in specified level"""
zoom_cache = self.cache_by_zoom[zoom_level]
if not zoom_cache:
return
# Find tile with earliest access time
oldest_key = min(zoom_cache.keys(), key=lambda k: zoom_cache[k][1])
del zoom_cache[oldest_key]
def _evict_least_used_zoom(self):
"""Remove some tiles from least used zoom level"""
if not self.cache_by_zoom:
return
# Calculate average access time for each level
zoom_scores = {}
for zoom_level, zoom_cache in self.cache_by_zoom.items():
if zoom_cache:
avg_access_time = sum(access_time for _, access_time in zoom_cache.values()) / len(zoom_cache)
zoom_scores[zoom_level] = avg_access_time
if not zoom_scores:
return
# Find level with earliest average access time
least_used_zoom = min(zoom_scores.keys(), key=lambda z: zoom_scores[z])
# Remove some oldest tiles from that level
zoom_cache = self.cache_by_zoom[least_used_zoom]
tiles_to_remove = min(30, len(zoom_cache) // 2)
if tiles_to_remove > 0:
# Sort by access time, remove oldest
sorted_tiles = sorted(zoom_cache.items(), key=lambda item: item[1][1])
for i in range(tiles_to_remove):
tile_key, _ = sorted_tiles[i]
del zoom_cache[tile_key]
# If level becomes empty, delete it
if not zoom_cache:
del self.cache_by_zoom[least_used_zoom]
self.stats["zoom_levels"].discard(least_used_zoom)
def has_tile(self, x, y, z):
"""Check if has specified tile"""
tile_key = self.get_tile_key(x, y, z)
return z in self.cache_by_zoom and tile_key in self.cache_by_zoom[z]
def clear_zoom_level(self, zoom_level):
"""Clear cache for specified level"""
if zoom_level in self.cache_by_zoom:
count = len(self.cache_by_zoom[zoom_level])
del self.cache_by_zoom[zoom_level]
self.stats["zoom_levels"].discard(zoom_level)
def clear_all(self):
"""Clear all cache"""
total_tiles = self.stats["total_tiles"]
self.cache_by_zoom.clear()
self.stats = {
"total_tiles": 0,
"zoom_levels": set(),
"cache_hits": self.stats["cache_hits"],
"cache_misses": self.stats["cache_misses"]
}
def get_stats(self):
"""Get cache statistics"""
# Update statistics
self.stats["total_tiles"] = sum(len(cache) for cache in self.cache_by_zoom.values())
# Statistics by level
zoom_stats = {}
for zoom_level, zoom_cache in self.cache_by_zoom.items():
zoom_stats[zoom_level] = len(zoom_cache)
return {
**self.stats,
"zoom_stats": zoom_stats,
"hit_rate": (self.stats["cache_hits"] /
(self.stats["cache_hits"] + self.stats["cache_misses"]) * 100)
if (self.stats["cache_hits"] + self.stats["cache_misses"]) > 0 else 0
}
class CompletedTileTracker:
"""Completed tile tracker"""
def __init__(self):
self.completed_by_zoom = defaultdict(set)
self.failed_by_zoom = defaultdict(set)
def mark_completed(self, x, y, z, success=True):
"""Mark tile completion status"""
tile_coord = (x, y)
if success:
self.completed_by_zoom[z].add(tile_coord)
self.failed_by_zoom[z].discard(tile_coord)
else:
self.failed_by_zoom[z].add(tile_coord)
def is_completed(self, x, y, z):
"""Check if tile is completed"""
tile_coord = (x, y)
return tile_coord in self.completed_by_zoom[z]
def is_failed(self, x, y, z):
"""Check if tile has failed"""
tile_coord = (x, y)
return tile_coord in self.failed_by_zoom[z]
def clear_zoom_level(self, zoom_level):
"""Clear tracking for specified level"""
self.completed_by_zoom[zoom_level].clear()
self.failed_by_zoom[zoom_level].clear()
def clear_all(self):
"""Clear all tracking"""
self.completed_by_zoom.clear()
self.failed_by_zoom.clear()
class TileRequest:
"""Tile request object"""
def __init__(self, x, y, z, priority=0):
self.x = x
self.y = y
self.z = z
self.priority = priority
self.url = None
self.reply = None
def __lt__(self, other):
return self.priority > other.priority
class QtNetworkTileLoader(QWidget):
"""Network tile loader - Fixed HTTP requests"""
tile_loaded = Signal(int, int, int, QPixmap)
log_message = Signal(str, str)
def __init__(self, max_concurrent=6):
super().__init__()
self.max_concurrent = max_concurrent
# Fixed URL template - using reliable tile service
self.tile_url_template = "http://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
# Backup service:
# self.tile_url_template = "http://wprd01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}"
# Network manager - simplified configuration
self.network_manager = QNetworkAccessManager(self)
self.network_manager.finished.connect(self.on_request_finished)
# Request queue and management
self.pending_requests = deque()
self.active_requests = {}
# Request processing timer
self.process_timer = QTimer()
self.process_timer.timeout.connect(self.process_pending_requests)
self.process_timer.start(50) # Reduced to 50ms for better responsiveness
# Statistics
self.request_count = 0
self.success_count = 0
self.error_count = 0
def add_tile_request(self, x, y, z, priority=0):
"""Add tile request"""
tile_key = (x, y, z)
# Check if already in queue
for request in self.pending_requests:
if (request.x, request.y, request.z) == tile_key:
return
# Check if currently downloading
for request in self.active_requests.values():
if (request.x, request.y, request.z) == tile_key:
return
# Create new request
request = TileRequest(x, y, z, priority)
request.url = self.tile_url_template.format(x=x, y=y, z=z)
# Add to queue and sort
self.pending_requests.append(request)
self.pending_requests = deque(sorted(self.pending_requests))
def clear_pending_requests(self):
"""Clear pending requests"""
cleared_count = len(self.pending_requests)
self.pending_requests.clear()
if cleared_count > 0:
self.log_message.emit(f"Cleared {cleared_count} pending requests", "INFO")
def process_pending_requests(self):
"""Process pending requests"""
while (len(self.active_requests) < self.max_concurrent and
len(self.pending_requests) > 0):
request = self.pending_requests.popleft()
self.start_request(request)
def start_request(self, request: TileRequest):
"""Start network request - reference successful code's simplified approach"""
try:
url = QUrl(request.url)
network_request = QNetworkRequest(url)
# Key fix: simplified request header setting, reference successful code
network_request.setRawHeader(b"User-Agent",
b"Mozilla/5.0 (Android; Mobile) UAVMap/1.0")
network_request.setRawHeader(b"Accept",
b"image/png,image/jpeg,image/*,*/*;q=0.8")
# Disable caching (reference successful code)
network_request.setAttribute(QNetworkRequest.Attribute.CacheLoadControlAttribute,
QNetworkRequest.CacheLoadControl.AlwaysNetwork)
# Set timeout - using new API
network_request.setTransferTimeout(10000) # 10 second timeout
# Start GET request - direct use, no additional SSL configuration needed
reply = self.network_manager.get(network_request)
request.reply = reply
# Record active request
self.active_requests[reply] = request
self.request_count += 1
# Add debug log
self.log_message.emit(f"Request started: {request.url}", "DEBUG")
except Exception as e:
self.log_message.emit(f"Request failed ({request.x}, {request.y}, {request.z}): {e}", "ERROR")
self.error_count += 1
def on_request_finished(self, reply: QNetworkReply):
"""Request completion callback - reference successful code's handling approach"""
try:
if reply not in self.active_requests:
reply.deleteLater()
return
request = self.active_requests.pop(reply)
# Check HTTP status code
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
if reply.error() == QNetworkReply.NetworkError.NoError and status_code == 200:
# Successfully got data
image_data = reply.readAll()
if image_data.size() > 0:
pixmap = QPixmap()
if pixmap.loadFromData(image_data):
self.tile_loaded.emit(request.x, request.y, request.z, pixmap)
self.success_count += 1
self.log_message.emit(
f"Tile loaded: ({request.x}, {request.y}, {request.z}) "
f"{image_data.size()}B HTTP:{status_code}", "SUCCESS")
else:
self.error_count += 1
self.log_message.emit(
f"Image parse failed ({request.x}, {request.y}, {request.z}) "
f"data size:{image_data.size()}B", "ERROR")
else:
self.error_count += 1
self.log_message.emit(f"Empty data ({request.x}, {request.y}, {request.z})", "ERROR")
else:
# Request failed
self.error_count += 1
error_details = []
if status_code:
error_details.append(f"HTTP:{status_code}")
if reply.error() != QNetworkReply.NetworkError.NoError:
error_details.append(f"Network error:{reply.errorString()}")
error_msg = f"Request failed ({request.x}, {request.y}, {request.z}): {' '.join(error_details)}"
self.log_message.emit(error_msg, "ERROR")
# If 404 error, log but don't retry
if status_code == 404:
self.log_message.emit(f"Tile not exists: L{request.z} ({request.x}, {request.y})", "WARNING")
except Exception as e:
self.log_message.emit(f"Response processing exception: {e}", "ERROR")
self.error_count += 1
finally:
reply.deleteLater()
def get_stats(self):
"""Get statistics"""
return {
"pending": len(self.pending_requests),
"active": len(self.active_requests),
"total_requests": self.request_count,
"success": self.success_count,
"errors": self.error_count,
"success_rate": (self.success_count / self.request_count * 100) if self.request_count > 0 else 0
}
class MapWidget(QWidget):
"""Map display component - Android touch optimized"""
def __init__(self, log_widget=None):
super().__init__()
self.setMinimumSize(800, 400)
self.log_widget = log_widget
# Android touch event configuration
self.setAttribute(Qt.WA_AcceptTouchEvents, True)
self.setMouseTracking(False) # Disable mouse tracking to save resources
# Map parameters
self.zoom_level = 15 # Start from lower level
self.view_center_x = -43.22723416
self.view_center_y = -22.85320108
self.tile_size = 256
# Marker point
self.marker_x = -43.22723416
self.marker_y = -22.85320108
# Cache configuration - optimized for mobile devices
self.tile_cache = TileCache(max_total_tiles=1000, max_tiles_per_zoom=300)
self.completed_tracker = CompletedTileTracker()
# Touch state management
self.touch_points = {} # Store current touch points {id: QPointF}
self.last_touch_points = {} # Store previous frame touch points
self.is_dragging = False
self.is_pinching = False
self.last_pinch_distance = 0.0
self.last_pinch_center = QPointF()
self.pinch_start_zoom = 15
# Touch threshold configuration
self.min_drag_distance = 5 # Minimum drag distance
self.min_pinch_distance_change = 20 # Minimum zoom distance change
self.double_tap_threshold = 300 # Double tap time threshold (ms)
self.touch_start_time = 0
# Gesture debouncing
self.gesture_timer = QTimer()
self.gesture_timer.setSingleShot(True)
self.gesture_timer.timeout.connect(self.end_gesture)
# Inertial scrolling
self.velocity_x = 0.0
self.velocity_y = 0.0
self.deceleration_timer = QTimer()
self.deceleration_timer.timeout.connect(self.update_inertia)
# Center marker settings
self.show_center_marker = True
# Create tile loader
self.tile_loader = QtNetworkTileLoader(max_concurrent=6)
self.tile_loader.tile_loaded.connect(self.on_tile_loaded)
# Connect log signals
if self.log_widget:
self.tile_loader.log_message.connect(self.log_widget.add_log)
# Create load timer
self.load_timer = QTimer()
self.load_timer.timeout.connect(self.load_visible_tiles)
self.load_timer.setSingleShot(True)
# Statistics update timer
self.stats_timer = QTimer()
self.stats_timer.timeout.connect(self.update_stats)
self.stats_timer.start(2000) # Update every 2 seconds
# Statistics
self.loader_stats = {"pending": 0, "active": 0}
self.cache_stats = {}
# Initial load
self.load_visible_tiles()
if self.log_widget:
self.log_widget.add_log("Map component initialized - Android simplified version", "SUCCESS")
self.log_widget.add_log("SSL and proxy config removed for Android compatibility", "INFO")
self.log_widget.add_log("Support single finger drag, dual finger zoom, double tap", "INFO")
def update_stats(self):
"""Update statistics"""
self.loader_stats = self.tile_loader.get_stats()
self.cache_stats = self.tile_cache.get_stats()
def deg2num(self, lat_deg, lon_deg, zoom):
"""Convert lat/lon to tile coordinates"""
lat_rad = math.radians(lat_deg)
n = 2.0 ** zoom
x = int((lon_deg + 180.0) / 360.0 * n)
y = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return x, y
def num2deg(self, x, y, zoom):
"""Convert tile coordinates to lat/lon"""
n = 2.0 ** zoom
lon_deg = x / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n)))
lat_deg = math.degrees(lat_rad)
return lat_deg, lon_deg
def latlon_to_screen(self, lat, lon):
"""Convert lat/lon to screen coordinates"""
center_tile_x, center_tile_y = self.deg2num(self.view_center_y, self.view_center_x, self.zoom_level)
target_tile_x, target_tile_y = self.deg2num(lat, lon, self.zoom_level)
center_lat, center_lon = self.num2deg(center_tile_x, center_tile_y, self.zoom_level)
next_lat, next_lon = self.num2deg(center_tile_x + 1, center_tile_y + 1, self.zoom_level)
center_lat_ratio = (self.view_center_y - center_lat) / (next_lat - center_lat)
center_lon_ratio = (self.view_center_x - center_lon) / (next_lon - center_lon)
center_offset_x = int(center_lon_ratio * self.tile_size)
center_offset_y = int(center_lat_ratio * self.tile_size)
target_lat, target_lon = self.num2deg(target_tile_x, target_tile_y, self.zoom_level)
target_next_lat, target_next_lon = self.num2deg(target_tile_x + 1, target_tile_y + 1, self.zoom_level)
target_lat_ratio = (lat - target_lat) / (target_next_lat - target_lat)
target_lon_ratio = (lon - target_lon) / (target_next_lon - target_lon)
target_offset_x = int(target_lon_ratio * self.tile_size)
target_offset_y = int(target_lat_ratio * self.tile_size)
widget_center_x = self.width() // 2
widget_center_y = self.height() // 2
screen_x = widget_center_x + (target_tile_x - center_tile_x) * self.tile_size + target_offset_x - center_offset_x
screen_y = widget_center_y + (target_tile_y - center_tile_y) * self.tile_size + target_offset_y - center_offset_y
return int(screen_x), int(screen_y)
def load_visible_tiles(self):
"""Load tiles in visible area"""
if self.zoom_level < 0 or self.zoom_level > 19:
return
center_tile_x, center_tile_y = self.deg2num(self.view_center_y, self.view_center_x, self.zoom_level)
tiles_x = (self.width() // self.tile_size) + 3 # Reduced preload range
tiles_y = (self.height() // self.tile_size) + 3
start_x = center_tile_x - tiles_x // 2
end_x = center_tile_x + tiles_x // 2
start_y = center_tile_y - tiles_y // 2
end_y = center_tile_y + tiles_y // 2
tiles_requested = 0
tiles_from_cache = 0
for x in range(start_x, end_x + 1):
for y in range(start_y, end_y + 1):
if x >= 0 and y >= 0 and x < 2**self.zoom_level and y < 2**self.zoom_level:
if self.tile_cache.has_tile(x, y, self.zoom_level):
tiles_from_cache += 1
continue
if (self.completed_tracker.is_completed(x, y, self.zoom_level) or
self.completed_tracker.is_failed(x, y, self.zoom_level)):
continue
distance = math.sqrt((x - center_tile_x)**2 + (y - center_tile_y)**2)
priority = 1000 - distance
self.tile_loader.add_tile_request(x, y, self.zoom_level, priority)
tiles_requested += 1
if self.log_widget and (tiles_requested > 0):
self.log_widget.add_log(f"L{self.zoom_level}: cached {tiles_from_cache} requested {tiles_requested}", "INFO")
def on_tile_loaded(self, x, y, z, pixmap):
"""Tile load completion callback"""
self.tile_cache.put(x, y, z, pixmap)
self.completed_tracker.mark_completed(x, y, z, success=True)
if z == self.zoom_level:
self.update()
def set_marker_position(self, lat, lon):
"""Set marker position"""
self.marker_x = lon
self.marker_y = lat
if self.log_widget:
self.log_widget.add_log(f"Marker: ({lat:.6f}, {lon:.6f})", "INFO")
self.update()
# Android touch event handling
def event(self, event):
"""Handle touch events"""
if event.type() == QTouchEvent.TouchBegin:
return self.touch_begin_event(event)
elif event.type() == QTouchEvent.TouchUpdate:
return self.touch_update_event(event)
elif event.type() == QTouchEvent.TouchEnd:
return self.touch_end_event(event)
else:
return super().event(event)
def touch_begin_event(self, event: QTouchEvent):
"""Touch begin event"""
self.touch_start_time = event.timestamp()
# Stop inertial scrolling
self.deceleration_timer.stop()
self.velocity_x = 0.0
self.velocity_y = 0.0
# Update touch points
self.touch_points.clear()
self.last_touch_points.clear()
for i in range(event.pointCount()):
touch_point = event.point(i)
pos = touch_point.position()
self.touch_points[touch_point.id()] = pos
self.last_touch_points[touch_point.id()] = pos
touch_count = len(self.touch_points)
if self.log_widget:
self.log_widget.add_log(f"Touch begin: {touch_count} point(s)", "TOUCH")
if touch_count == 1:
# Single touch - prepare for drag
self.is_dragging = False
self.is_pinching = False
elif touch_count == 2:
# Dual touch - prepare for zoom
self.is_dragging = False
self.is_pinching = True
points = list(self.touch_points.values())
self.last_pinch_distance = self.calculate_distance(points[0], points[1])
self.last_pinch_center = self.calculate_center(points[0], points[1])
self.pinch_start_zoom = self.zoom_level
if self.log_widget:
self.log_widget.add_log(f"Pinch start: distance {self.last_pinch_distance:.1f}", "TOUCH")
return True
def touch_update_event(self, event: QTouchEvent):
"""Touch update event"""
# Update current touch points
current_points = {}
for i in range(event.pointCount()):
touch_point = event.point(i)
current_points[touch_point.id()] = touch_point.position()
touch_count = len(current_points)
if touch_count == 1 and not self.is_pinching:
# Single point drag
touch_id = list(current_points.keys())[0]
if touch_id in self.last_touch_points:
current_pos = current_points[touch_id]
last_pos = self.last_touch_points[touch_id]
delta = current_pos - last_pos
# Check if dragging starts
if not self.is_dragging:
total_delta = current_pos - self.touch_points[touch_id]
distance = math.sqrt(total_delta.x()**2 + total_delta.y()**2)
if distance > self.min_drag_distance:
self.is_dragging = True
if self.log_widget:
self.log_widget.add_log("Map dragging started", "TOUCH")
if self.is_dragging:
# Apply map dragging
scale = 360.0 / (2**self.zoom_level * self.tile_size)
self.view_center_x -= delta.x() * scale
self.view_center_y += delta.y() * scale * math.cos(math.radians(self.view_center_y))
# Limit range
self.view_center_x = max(-180, min(180, self.view_center_x))
self.view_center_y = max(-85, min(85, self.view_center_y))
# Calculate velocity for inertial scrolling
self.velocity_x = delta.x() * 0.5
self.velocity_y = delta.y() * 0.5
self.update()
elif touch_count == 2 and self.is_pinching:
# Dual point zoom
points = list(current_points.values())
current_distance = self.calculate_distance(points[0], points[1])
current_center = self.calculate_center(points[0], points[1])
# Calculate zoom
if self.last_pinch_distance > 0:
distance_change = current_distance - self.last_pinch_distance
if abs(distance_change) > self.min_pinch_distance_change:
if distance_change > 0:
# Zoom in
if self.zoom_level < 19:
self.zoom_in()
self.last_pinch_distance = current_distance
if self.log_widget:
self.log_widget.add_log(f"Pinch zoom in to L{self.zoom_level}", "TOUCH")
else:
# Zoom out
if self.zoom_level > 1:
self.zoom_out()
self.last_pinch_distance = current_distance
if self.log_widget:
self.log_widget.add_log(f"Pinch zoom out to L{self.zoom_level}", "TOUCH")
# Update previous frame touch points
self.last_touch_points = current_points.copy()
return True
def touch_end_event(self, event: QTouchEvent):
"""Touch end event"""
# Fix: use new API to replace deprecated touchPoints()
remaining_points = {}
for i in range(event.pointCount()):
touch_point = event.point(i)
# Use correct state checking method
touch_state = touch_point.state()
is_released = (touch_state == Qt.TouchPointState.TouchPointReleased)
if not is_released:
remaining_points[touch_point.id()] = touch_point.position()
touch_duration = event.timestamp() - self.touch_start_time
if len(remaining_points) == 0:
# All touch points ended
if self.log_widget:
self.log_widget.add_log(f"Touch end: duration {touch_duration}ms", "TOUCH")
# Check if double tap
if (not self.is_dragging and not self.is_pinching and
touch_duration < self.double_tap_threshold and len(self.touch_points) == 1):
# Double tap zoom in
if self.zoom_level < 19:
self.zoom_in()
if self.log_widget:
self.log_widget.add_log(f"Double tap zoom in to L{self.zoom_level}", "TOUCH")
# Start inertial scrolling
if self.is_dragging and (abs(self.velocity_x) > 1 or abs(self.velocity_y) > 1):
self.deceleration_timer.start(16) # 60fps
if self.log_widget:
self.log_widget.add_log("Inertial scrolling started", "TOUCH")
# Reset states
self.is_dragging = False
self.is_pinching = False
self.touch_points.clear()
self.last_touch_points.clear()
# Delayed tile loading
self.load_timer.start(300)
else:
# Still have touch points, update state
self.touch_points = remaining_points
self.last_touch_points = remaining_points.copy()
if len(remaining_points) == 1:
# From dual to single point
self.is_pinching = False
return True
def calculate_distance(self, p1: QPointF, p2: QPointF):
"""Calculate distance between two points"""
dx = p2.x() - p1.x()
dy = p2.y() - p1.y()
return math.sqrt(dx * dx + dy * dy)
def calculate_center(self, p1: QPointF, p2: QPointF):
"""Calculate center of two points"""
return QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
def update_inertia(self):
"""Update inertial scrolling"""
# Apply velocity
scale = 360.0 / (2**self.zoom_level * self.tile_size)
self.view_center_x -= self.velocity_x * scale
self.view_center_y += self.velocity_y * scale * math.cos(math.radians(self.view_center_y))
# Limit range
self.view_center_x = max(-180, min(180, self.view_center_x))
self.view_center_y = max(-85, min(85, self.view_center_y))
# Deceleration
self.velocity_x *= 0.95
self.velocity_y *= 0.95
# Stop condition
if abs(self.velocity_x) < 0.1 and abs(self.velocity_y) < 0.1:
self.deceleration_timer.stop()
self.velocity_x = 0.0
self.velocity_y = 0.0
self.load_timer.start(100)
self.update()
def end_gesture(self):
"""End gesture"""
self.load_timer.start(200)
def draw_center_marker(self, painter: QPainter):
"""Draw center marker"""
if not self.show_center_marker:
return
screen_x, screen_y = self.latlon_to_screen(self.marker_y, self.marker_x)
if (screen_x < -50 or screen_x > self.width() + 50 or
screen_y < -50 or screen_y > self.height() + 50):
return
# Draw marker - optimized for touch screen, larger size
painter.setPen(QPen(Qt.GlobalColor.red, 2))
painter.setBrush(QBrush(Qt.GlobalColor.red))
# Center circle
painter.drawEllipse(screen_x - 6, screen_y - 6, 12, 12)
# Cross lines
painter.setPen(QPen(Qt.GlobalColor.white, 3))
painter.drawLine(screen_x - 20, screen_y, screen_x + 20, screen_y)
painter.drawLine(screen_x, screen_y - 20, screen_x, screen_y + 20)
# North arrow
arrow_points = [
QPoint(screen_x, screen_y - 40),
QPoint(screen_x - 10, screen_y - 28),
QPoint(screen_x - 4, screen_y - 28),
QPoint(screen_x - 4, screen_y - 18),
QPoint(screen_x + 4, screen_y - 18),
QPoint(screen_x + 4, screen_y - 28),
QPoint(screen_x + 10, screen_y - 28),
]
arrow_polygon = QPolygon(arrow_points)
painter.setPen(QPen(Qt.GlobalColor.red, 1))
painter.setBrush(QBrush(Qt.GlobalColor.red))
painter.drawPolygon(arrow_polygon)
painter.setPen(QPen(Qt.GlobalColor.red, 3))
painter.drawText(screen_x + 15, screen_y - 28, "N")
def draw_debug_info(self, painter: QPainter):
"""Draw debug info - optimized for touch screen"""
# Current level tile count
current_zoom_tiles = self.cache_stats.get("zoom_stats", {}).get(self.zoom_level, 0)
# Cache hit rate
hit_rate = self.cache_stats.get("hit_rate", 0)
# Network request success rate
success_rate = self.loader_stats.get("success_rate", 0)
# Touch status
touch_status = []
if self.is_dragging:
touch_status.append("drag")
if self.is_pinching:
touch_status.append("pinch")
if self.deceleration_timer.isActive():
touch_status.append("inertia")
touch_status_str = " + ".join(touch_status) if touch_status else "idle"
# Use larger font for touch screen
font = QFont()
font.setPointSize(10)
painter.setFont(font)
# Draw text shadow
painter.setPen(QPen(Qt.black, 2))
y_offset = 25
painter.drawText(11, y_offset, f"ArcGIS L{self.zoom_level} tiles:{current_zoom_tiles}")
y_offset += 22
painter.drawText(11, y_offset, f"Cache:{self.cache_stats.get('total_tiles', 0)} hit:{hit_rate:.0f}%")
y_offset += 22
painter.drawText(11, y_offset, f"Network: {self.loader_stats['pending']}queue {self.loader_stats['active']}dl")
y_offset += 22
painter.drawText(11, y_offset, f"Success:{success_rate:.0f}% [no SSL/proxy]")
y_offset += 22
painter.drawText(11, y_offset, f"Center:({self.view_center_y:.3f},{self.view_center_x:.3f})")
y_offset += 22
painter.drawText(11, y_offset, f"Touch:{len(self.touch_points)}pt {touch_status_str}")
# Draw white text
painter.setPen(QPen(Qt.white, 1))
y_offset = 24
painter.drawText(10, y_offset, f"ArcGIS L{self.zoom_level} tiles:{current_zoom_tiles}")
y_offset += 22
painter.drawText(10, y_offset, f"Cache:{self.cache_stats.get('total_tiles', 0)} hit:{hit_rate:.0f}%")
y_offset += 22
painter.drawText(10, y_offset, f"Network: {self.loader_stats['pending']}queue {self.loader_stats['active']}dl")
y_offset += 22
painter.drawText(10, y_offset, f"Success:{success_rate:.0f}% [no SSL/proxy]")
y_offset += 22
painter.drawText(10, y_offset, f"Center:({self.view_center_y:.3f},{self.view_center_x:.3f})")
y_offset += 22
painter.drawText(10, y_offset, f"Touch:{len(self.touch_points)}pt {touch_status_str}")
def paintEvent(self, event):
"""Draw map"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Fill background
painter.fillRect(self.rect(), Qt.lightGray)
if self.zoom_level < 0 or self.zoom_level > 19:
return
# Calculate view parameters
center_tile_x, center_tile_y = self.deg2num(self.view_center_y, self.view_center_x, self.zoom_level)
center_lat, center_lon = self.num2deg(center_tile_x, center_tile_y, self.zoom_level)
next_lat, next_lon = self.num2deg(center_tile_x + 1, center_tile_y + 1, self.zoom_level)
lat_ratio = (self.view_center_y - center_lat) / (next_lat - center_lat)
lon_ratio = (self.view_center_x - center_lon) / (next_lon - center_lon)
offset_x = int(lon_ratio * self.tile_size)
offset_y = int(lat_ratio * self.tile_size)
widget_center_x = self.width() // 2
widget_center_y = self.height() // 2
tiles_x = (self.width() // self.tile_size) + 2
tiles_y = (self.height() // self.tile_size) + 2
start_x = center_tile_x - tiles_x // 2
end_x = center_tile_x + tiles_x // 2
start_y = center_tile_y - tiles_y // 2
end_y = center_tile_y + tiles_y // 2
# Draw tiles
for x in range(start_x, end_x + 1):
for y in range(start_y, end_y + 1):
if x >= 0 and y >= 0 and x < 2**self.zoom_level and y < 2**self.zoom_level:
# Get tile from cache
pixmap = self.tile_cache.get(x, y, self.zoom_level)
if pixmap:
screen_x = widget_center_x + (x - center_tile_x) * self.tile_size - offset_x
screen_y = widget_center_y + (y - center_tile_y) * self.tile_size - offset_y
painter.drawPixmap(screen_x, screen_y, pixmap)
# Draw markers and debug info
self.draw_center_marker(painter)
self.draw_debug_info(painter)
def zoom_in(self):
"""Zoom in"""
if self.zoom_level < 19:
old_zoom = self.zoom_level
self.zoom_level += 1
self.tile_loader.clear_pending_requests()
self.load_visible_tiles()
self.update()
def zoom_out(self):
"""Zoom out"""
if self.zoom_level > 1:
old_zoom = self.zoom_level
self.zoom_level -= 1
self.tile_loader.clear_pending_requests()
self.load_visible_tiles()
self.update()
def set_center(self, lat, lon):
"""Set map center"""
self.view_center_x = lon
self.view_center_y = lat
if self.log_widget:
self.log_widget.add_log(f"Map center: ({lat:.6f}, {lon:.6f})", "INFO")
self.tile_loader.clear_pending_requests()
self.load_visible_tiles()
self.update()
class MapWindow(QMainWindow):
"""Main window - Android simplified version"""
def __init__(self):
super().__init__()
self.setWindowTitle("UAV Map - Android Simplified")
self.setGeometry(50, 50, 400, 800) # Adapt to mobile screen ratio
# Set Android style
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QMenuBar {
background-color: #2196f3;
color: white;
font-size: 16px;
padding: 8px;
}
QMenuBar::item {
background-color: transparent;
padding: 8px 12px;
}
QMenuBar::item:selected {
background-color: #1976d2;
border-radius: 4px;
}
QMenu {
background-color: white;
border: 1px solid #ddd;
font-size: 14px;
}
QMenu::item {
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
QMenu::item:selected {
background-color: #e3f2fd;
}
""")
# Create menu bar
self.create_menu()
# Create splitter
self.main_splitter = QSplitter(Qt.Vertical)
self.setCentralWidget(self.main_splitter)
# Create log component
self.log_widget = LogWidget()
# Create map component
self.map_widget = MapWidget(self.log_widget)
# Add to splitter
self.main_splitter.addWidget(self.map_widget)
self.main_splitter.addWidget(self.log_widget)
# Set split ratio (map 75%, log 25%)
self.main_splitter.setSizes([600, 200])
# Limit log window minimum height
self.log_widget.setMinimumHeight(120)
# Add initial logs
self.log_widget.add_log("UAV Map Android simplified version started", "SUCCESS")
self.log_widget.add_log("SSL and proxy config removed", "INFO")
self.log_widget.add_log("Multi-touch operation supported", "INFO")
self.log_widget.add_log("• Single finger drag: pan map", "INFO")
self.log_widget.add_log("• Dual finger pinch: zoom map", "INFO")
self.log_widget.add_log("• Double tap: quick zoom in", "INFO")
self.log_widget.add_log("• Inertial scrolling: smooth drag experience", "INFO")
def clear_cache(self):
"""Clear cache"""
self.map_widget.tile_cache.clear_all()
self.map_widget.completed_tracker.clear_all()
self.map_widget.load_visible_tiles()
self.map_widget.update()
self.log_widget.add_log("All cache cleared", "WARNING")
def clear_current_zoom_cache(self):
"""Clear current zoom level cache only"""
current_zoom = self.map_widget.zoom_level
self.map_widget.tile_cache.clear_zoom_level(current_zoom)
self.map_widget.completed_tracker.clear_zoom_level(current_zoom)
self.map_widget.load_visible_tiles()
self.map_widget.update()
self.log_widget.add_log(f"Level {current_zoom} cache cleared", "WARNING")
def create_menu(self):
"""Create menu bar - touch friendly (proxy settings removed)"""
menubar = self.menuBar()
# View menu
view_menu = menubar.addMenu("🌍 Location")
view_menu.addSeparator()
toggle_marker_action = QAction("📍 Toggle Marker", self)
toggle_marker_action.triggered.connect(self.toggle_marker)
view_menu.addAction(toggle_marker_action)
# Settings menu (proxy settings removed)
settings_menu = menubar.addMenu("⚙️ Settings")
clear_all_action = QAction("🗑️ Clear All Cache", self)
clear_all_action.triggered.connect(self.clear_cache)
settings_menu.addAction(clear_all_action)
clear_current_action = QAction("🔄 Clear Current Level", self)
clear_current_action.triggered.connect(self.clear_current_zoom_cache)
settings_menu.addAction(clear_current_action)
settings_menu.addSeparator()
clear_log_action = QAction("📝 Clear Log", self)
clear_log_action.triggered.connect(lambda: self.log_widget.clear_log())
settings_menu.addAction(clear_log_action)
# Help menu
about_menu = menubar.addMenu("❓ Help")
help_action = QAction("📖 Instructions", self)
help_action.triggered.connect(self.show_help)
about_menu.addAction(help_action)
about_action = QAction("ℹ️ About", self)
about_action.triggered.connect(self.show_about)
about_menu.addAction(about_action)
def goto_location(self, lat, lon, name):
"""Go to specified location"""
self.map_widget.set_center(lat, lon)
self.map_widget.set_marker_position(lat, lon)
self.log_widget.add_log(f"Jumped to {name}", "INFO")
def toggle_marker(self):
"""Toggle marker display"""
self.map_widget.show_center_marker = not self.map_widget.show_center_marker
status = "shown" if self.map_widget.show_center_marker else "hidden"
self.log_widget.add_log(f"Marker {status}", "INFO")
self.map_widget.update()
def show_help(self):
"""Show operation help"""
QMessageBox.information(self, "Instructions",
"🗺️ UAV Map Android Simplified Version Guide\n\n")
def show_about(self):
"""Show about dialog"""
QMessageBox.about(self, "About",
"🛰️ UAV Map - Android Simplified Version\n\n")
def main():
app = QApplication(sys.argv)
app.setApplicationName("UAV MAP Android")
app.setOrganizationName("ZengZhengRong")
# Android specific configuration
app.setAttribute(Qt.AA_SynthesizeTouchForUnhandledMouseEvents, True)
app.setAttribute(Qt.AA_SynthesizeMouseForUnhandledTouchEvents, False)
# Get icon path
try:
logo_path = get_resource_path("assets", "logo", "uav.ico")
icon = QIcon()
icon.addPixmap(QPixmap(logo_path))
app.setWindowIcon(icon)
except:
pass
window = MapWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Above code window is work ,or do you have any solutions to handle network requests
I follow the script and commands of Pyside6 6.9.1, and my system is ubuntu 24.04
Metadata
Metadata
Assignees
Labels
No labels