Skip to content

QNetworkAccessManager SSL/TLS initialization fails #9

@zengzhengrong

Description

@zengzhengrong

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions