• 实战PyQt5: 132-一个轻量级的地图应用


    地图显示也是在桌面应用中也是一个普遍的需求,地图应用有一个特点是,从网络上下载数据的数据量很大,地图应用需要在不同地区之间进行重复切换,如果每次切换到不同地区,就重新从网络上下载数据,这样做不仅浪费网络流量,而且显示的实时性很很差。但地图数据有个显著的特点就是数据更新很缓慢,如果可以使用某种缓存模式,保存地图数据,则在不同地区间切换时,已经有缓存数据的地区就无需重新下载,直接调用缓冲数据显示即可,这样可完美解决前面提到的问题。

    在PyQt中,类QNetworkDiskCache提供了基本的磁盘缓存,可以用来达到上述目的,在后面我们将演示使用磁盘缓冲技术,使用HTTP请求,从网络中获取不同城市的数据,并显示在窗口中。

    QNetworkDiskCache

    QNetworkDiskCache使用QDataStream将每个URL存储在cacheDirectory内部的自己的文件中。带有文本MimeType的文件使用qCompress压缩。数据仅在insert()和updateMetaData()中写入磁盘。默认情况下,QNetworkDiskCache将缓存在系统上使用的空间量限制为50MB。必须先设置缓存目录,然后它才能起作用。

    可以通过以下方式启动网络磁盘缓存:

    1. nam = QNetworkAccessManager(self)
    2. diskCache = QNetworkDiskCache(self)
    3. diskCache.setCacheDirectory('cacheDir')
    4. nam.setCache(diskcache)

    发送请求时,需要控制何时使用缓存,何时使用网络:

    1. #发出常规请求(默认为网络请求,因为它是默认设置)
    2. request = QNetworkRequest(QUrl('http://qt-project.org'))
    3. nam.get(request)
    4. #发出一个从缓存获取数据的请求
    5. request2 = QNetworkRequest(QUrl('http://qt-project.org'))
    6. request2.setAttribute(QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.PreferCache)
    7. manager.get(request2)

    要检查响应是来自网络还是缓存,可使用下面的方式:

    1. void replyFinished(self, reply):
    2.          fromCache = reply->attribute(QNetworkRequest.SourceIsFromCacheAttribute)
    3.          print('来自缓存中的网页?{}'.format(fromCache)

    QNetworkDiskCache的常用函数:

    • expire(self): 清理缓存,使其大小小于最大缓存大小。返回缓存的当前大小。当高速缓存的当前大小大于maximumCacheSize()时,将删除较早的高速缓存文件,直到总大小小于maxmaxCacheSize()的90%。
    • clear(self): 清除缓存中的所有数据,如果清理成功,则调用cacheSize()将返回0。
    • fileMetaData(self, filename): 返回缓存文件fileName的QNetworkCacheMetaData。如果fileName不是缓存文件,则QNetworkCacheMetaData将无效。
    • insert(self, device): 将数据插入device并将准备好的元数据插入缓存。
    • prepare(self, metaData): 返回应使用缓存项metaData填充数据的设备。
    • remove(self, url): 删除url的缓存项,如果成功则返回True,否则返回False。
    • data(self, url): 返回与url关联的数据。完成此操作后,由应用程序请求数据删除QIODevice。
    • updateMetaData(self, metaData): 将元数据网址的缓存元数据更新为metaData,如果缓存不包含该URL的缓存项,则不采取任何措施。
    • metaData(self): 返回URL地址的元数据url。如果url有效,并且缓存包含url的数据,则返回有效的QNetworkCacheMetaData。
    • cacheSize(self): 返回缓存占用的当前尺寸。根据缓存的实现,它可能是磁盘或内存的尺寸。
    • setMaximumCacheSize(self, size): 将磁盘缓存的最大尺寸设置为size。如果新尺寸小于当前的缓存尺寸,则缓存将调用expire()。
    • maximumCacheSize(self): 返回磁盘缓存设置的最大尺寸。
    • setCacheDirectory(self, cacheDir): 设置将缓存文件存储到cacheDir的目录,如果该目录不存在,QNetworkDiskCache将创建该目录。
    • cacheDirectory(self): 返回将存储缓存文件的位置。

    实现一个简单的地图应用

    使用openstreetmap.org提供的地图信息,参考pyqt5-examples样例代码,代码演示了如何使用经纬度,来显示一个城市的地图.完整代码如下:

    1. import sys,math
    2. from PyQt5.QtCore import (Qt, pyqtSignal, QObject, QPoint, QPointF,
    3.                           QRect, QSize, QStandardPaths, QUrl)
    4. from PyQt5.QtGui import (QColor, QImage, QPixmap, QPainter, QPainterPath,
    5.                          QRadialGradient, QDesktopServices)
    6. from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QMenuBar, 
    7.                              QMenu, QAction,  QActionGroup)
    8. from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest, QNetworkDiskCache)
    9.  
    10. #瓷砖尺寸(以像素为单位)
    11. # tile(瓷砖),我们的地图图像就是有一小块小块的图像平铺而成,就是铺瓷砖,形成整个地面
    12. TILE_DIM = 256
    13.  
    14. class MyPoint(QPoint):
    15.     def __init__(self, *par):
    16.         if par:
    17.             super(MyPoint, self).__init__(*par)
    18.         else:
    19.             super(MyPoint, self).__init__()
    20.     
    21.     #提供hash值,作为字典的键值        
    22.     def __hash__(self):
    23.         return self.x() * 17 ^ self.y()
    24.     
    25.     def __repr__(self):
    26.         return 'Point(%s, %s)' % (self.x(), self.y())
    27.     
    28. #根据地图坐标(维度lat, 经度lng, 缩放系数zoom),计算瓷砖位置
    29. def tileForCoordinate(lat, lng, zoom):
    30.     zn = float(1 << zoom)
    31.     tx = float(lng + 180.0) / 360.0
    32.     ty = (1.0 - math.log(math.tan(lat * math.pi / 180.0) +
    33.           1.0 / math.cos(lat * math.pi / 180.0)) / math.pi) / 2.0
    34.     
    35.     return QPointF(tx * zn, ty * zn)
    36.  
    37. #根据瓷砖x方向位置计算经度
    38. def longitudeFromTile(tx, zoom):
    39.     zn = float(1 << zoom)
    40.     lng = tx / zn * 360.0 - 180.0
    41.     return lng
    42.  
    43. #根据瓷砖y方向位置计算纬度
    44. def latitudeFromTile(ty, zoom):
    45.     zn = float(1 << zoom)
    46.     n = math.pi - 2 * math.pi * ty / zn
    47.     lat = 180.0 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n)))
    48.     return lat
    49.  
    50. class MyMap(QObject):
    51.     updated = pyqtSignal(QRect)
    52.     
    53.     def __init__(self, parent = None):
    54.         super(MyMap, self).__init__(parent)
    55.         
    56.         self._offset = QPoint()     
    57.         self._tilesRect = QRect()   
    58.         self._tilePixmaps = {} # MyPoint(x, y) to QPixmap mapping
    59.         self._nam = QNetworkAccessManager()
    60.         self._url = QUrl()      
    61.         #公共变量
    62.         self.width = 480      
    63.         self.height = 300
    64.         self.zoom = 14  #地图缩放系数
    65.         self.latitude = 39.9   #纬度
    66.         self.longitude = 116.4 #经度
    67.         
    68.         self._emptyTile = QPixmap(TILE_DIM, TILE_DIM)
    69.         self._emptyTile.fill(Qt.lightGray)
    70.         
    71.         cache = QNetworkDiskCache()
    72.         cache.setCacheDirectory(QStandardPaths.writableLocation(QStandardPaths.CacheLocation))
    73.         self._nam.setCache(cache)
    74.         self._nam.finished.connect(self.handleNetworkData)
    75.         
    76.     #刷新
    77.     def invalidate(self):
    78.         if self.width <= 0 or self.height <= 0:
    79.             return
    80.         ct = tileForCoordinate(self.latitude, self.longitude, self.zoom)
    81.         tx = ct.x()
    82.         ty = ct.y()
    83.         
    84.         # 中心位置瓷砖的左上角坐标
    85.         xp = int(self.width / 2 - (tx - math.floor(tx)) * TILE_DIM)
    86.         yp = int(self.height / 2 - (ty - math.floor(ty)) * TILE_DIM)
    87.         
    88.         #第一块瓷砖水平和垂直起始位置
    89.         xa = (xp + TILE_DIM - 1) / TILE_DIM
    90.         ya = (yp + TILE_DIM - 1) / TILE_DIM
    91.         xs = int(tx) - xa
    92.         ys = int(ty) - ya
    93.         
    94.         #左上角瓷砖的位置偏移量
    95.         self._offset = QPoint(xp - xa * TILE_DIM, yp - ya * TILE_DIM)
    96.         
    97.         #最后一块瓷砖水平和垂直结束位置
    98.         xe = int(tx) + (self.width - xp - 1) / TILE_DIM
    99.         ye = int(ty) + (self.height - yp - 1) / TILE_DIM
    100.         
    101.         #构建整个地图平铺区域
    102.         self._tilesRect = QRect(xs, ys, xe - xs + 1, ye - ys + 1)
    103.         
    104.         if self._url.isEmpty():
    105.             self.download()
    106.             
    107.         #通知刷新
    108.         self.updated.emit(QRect(00, self.width, self.height))
    109.         
    110.     #渲染
    111.     def render(self, p, rect):
    112.         for x in range(self._tilesRect.width()):
    113.             for y in range(self._tilesRect.height()):
    114.                 tp = MyPoint(x + self._tilesRect.left(), y + self._tilesRect.top())
    115.                 box = self.tileRect(tp)
    116.                 if rect.intersects(box):
    117.                     p.drawPixmap(box, self._tilePixmaps.get(tp, self._emptyTile))
    118.     
    119.     #平移
    120.     def pan(self, delta):
    121.         dx = QPointF(delta) / float(TILE_DIM)
    122.         center = tileForCoordinate(self.latitude, self.longitude, self.zoom) - dx
    123.         self.latitude = latitudeFromTile(center.y(), self.zoom)
    124.         self.longitude = longitudeFromTile(center.x(), self.zoom)
    125.         self.invalidate()
    126.         
    127.     #slots, 处理接收到的网路数据
    128.     def handleNetworkData(self, reply):
    129.         img = QImage()
    130.         tp = MyPoint(reply.request().attribute(QNetworkRequest.User))
    131.         if not reply.error():
    132.             #将下载的数据保存到位图中
    133.             if img.load(reply, None):
    134.                 self._tilePixmaps[tp] = QPixmap.fromImage(img)
    135.             reply.deleteLater()
    136.             self.updated.emit(self.tileRect(tp))
    137.             
    138.         #将为使用的瓷砖清理掉
    139.         bound = self._tilesRect.adjusted(-2, -222)
    140.         for tp in list(self._tilePixmaps.keys()):
    141.             if not bound.contains(tp):
    142.                 del self._tilePixmaps[tp]
    143.         
    144.         self.download()
    145.         
    146.     #从网络上下载数据
    147.     def download(self):
    148.         grab = None
    149.         #计算抓取位置
    150.         for x in range(self._tilesRect.width()):
    151.             for y in range(self._tilesRect.height()):
    152.                 tp = MyPoint(self._tilesRect.topLeft() + QPoint(x, y))
    153.                 if tp not in self._tilePixmaps:
    154.                     grab = QPoint(tp)
    155.                     break
    156.         
    157.         if grab is None:
    158.             self._url = QUrl()
    159.             return
    160.          
    161.         path = 'http://tile.openstreetmap.org/%d/%d/%d.png' % (self.zoom, grab.x(), grab.y())
    162.         self._url = QUrl(path)
    163.         request = QNetworkRequest()
    164.         request.setUrl(self._url)
    165.         request.setAttribute(QNetworkRequest.User, grab)
    166.         self._nam.get(request)
    167.         
    168.     def tileRect(self, tp):
    169.         t = tp - self._tilesRect.topLeft()
    170.         x = t.x() * TILE_DIM + self._offset.x()
    171.         y = t.y() * TILE_DIM + self._offset.y() 
    172.         
    173.         return QRect(x, y, TILE_DIM, TILE_DIM)  
    174.     
    175. #地图显示交互部件
    176. class LightMaps(QWidget):  
    177.     def __init__(self, parent = None):
    178.         super(LightMaps, self).__init__(parent)
    179.         
    180.         self.invert = False #白天夜晚模式切换
    181.         
    182.         self._normalMap = MyMap(self)     
    183.         self._normalMap.updated.connect(self.updateMap)
    184.      
    185.     #设置中心位置   
    186.     def setCenter(self, lat, lng):
    187.         self._normalMap.latitude = lat
    188.         self._normalMap.longitude = lng
    189.         self._normalMap.invalidate()
    190.         
    191.     #slot
    192.     def toggleNightMode(self):
    193.         self.invert = not self.invert
    194.         self.update()
    195.     
    196.     #更新地图    
    197.     def updateMap(self, rct):
    198.         self.update(rct)
    199.     
    200.     def resizeEvent(self, event):
    201.         self._normalMap.width = self.width()
    202.         self._normalMap.height = self.height()
    203.         self._normalMap.invalidate()
    204.         
    205.     def paintEvent(self, event):
    206.         p = QPainter()
    207.         p.begin(self)
    208.         self._normalMap.render(p, event.rect())
    209.         p.end()
    210.         
    211.         if self.invert:
    212.             p = QPainter(self)
    213.             p.setCompositionMode(QPainter.CompositionMode_Difference)
    214.             p.fillRect(event.rect(), Qt.white)
    215.             p.end()
    216.         
    217.  
    218. class DemoLightMap(QMainWindow):
    219.     def __init__(self, parent=None):
    220.         super(DemoLightMap, self).__init__(parent)   
    221.         
    222.          # 设置窗口标题
    223.         self.setWindowTitle('实战Qt for Python: 一个轻量级的地图应用')      
    224.         # 设置窗口大小
    225.         self.resize(640480)
    226.         
    227.         self.lightMap = LightMaps(self)
    228.         self.setCentralWidget(self.lightMap)
    229.         self.lightMap.setFocus()
    230.       
    231.         self.initUi()
    232.         
    233.     def initUi(self):
    234.         self.initMenuBar()
    235.     
    236.     def initMenuBar(self):
    237.         menuBar = self.menuBar() 
    238.         menuFile = menuBar.addMenu('文件(&F)')
    239.         menuOption = menuBar.addMenu('操作(&O')
    240.         menuHelp = menuBar.addMenu('帮助(&H)')
    241.         
    242.         actionExit = QAction('退出(&X)', self)
    243.         actionExit.triggered.connect(QApplication.instance().quit)
    244.         menuFile.addAction(actionExit)
    245.         
    246.         aBeijing = QAction('北京(&B)', self)
    247.         aBeijing.setCheckable(True)
    248.         aBeijing.setChecked(True)
    249.         aBeijing.triggered.connect(lambda: self.chooseCity(39.92116.46))
    250.         aShangHai = QAction('上海(&S)', self)
    251.         aShangHai.setCheckable(True)
    252.         aShangHai.setChecked(False)
    253.         aShangHai.triggered.connect(lambda: self.chooseCity(31.22121.48))
    254.         aTianjin = QAction('天津(&T)', self)
    255.         aTianjin.setCheckable(True)
    256.         aTianjin.setChecked(False)
    257.         aTianjin.triggered.connect(lambda: self.chooseCity(39.13117.2))
    258.         aChongQing = QAction('重庆(&C)', self)
    259.         aChongQing.setCheckable(True)
    260.         aChongQing.setChecked(False)
    261.         aChongQing.triggered.connect(lambda: self.chooseCity(29.55106.58))
    262.         
    263.         aGrp = QActionGroup(self)
    264.         aGrp.addAction(aBeijing)
    265.         aGrp.addAction(aShangHai)
    266.         aGrp.addAction(aTianjin)
    267.         aGrp.addAction(aChongQing)
    268.         
    269.         aNightMode = QAction('夜晚模式', self)
    270.         aNightMode.setCheckable(True)
    271.         aNightMode.setChecked(False)
    272.         aNightMode.triggered.connect(self.lightMap.toggleNightMode)  
    273.         
    274.         menuOption.addAction(aBeijing)
    275.         menuOption.addAction(aShangHai)
    276.         menuOption.addAction(aTianjin)
    277.         menuOption.addAction(aChongQing)
    278.         menuOption.addSeparator()
    279.         menuOption.addAction(aNightMode)
    280.         
    281.         osmAction = QAction("关于OpenStreetMap", self)
    282.         osmAction.triggered.connect(self.aboutOsm)
    283.         menuHelp.addAction(osmAction)
    284.         
    285.     def chooseCity(self, lat, lng):
    286.         self.lightMap.setCenter(lat, lng)
    287.         
    288.     def aboutOsm(self):
    289.         QDesktopServices.openUrl(QUrl('http://www.openstreetmap.org'))
    290.                  
    291. if __name__ == '__main__':
    292.     app = QApplication(sys.argv)
    293.     window = DemoLightMap()
    294.     window.show()
    295.     sys.exit(app.exec())   

    运行结果如下图:

     

    前一篇:实战PyQt5: 131-使用HTTP请求获得城市天气信息

  • 相关阅读:
    领域模型优先于数据库表
    Shell:一键部署pxe
    拦截|篡改|伪造.NET类库中不限于public的类和方法
    Linux【进程间通信】
    服务网格和性能优化:介绍如何通过服务网格提高微服务架构的性能和可扩展性
    从Spring源码探究IOC初始化流程
    【论文笔记】—低照度图像增强—Supervised—GLADNet—2018-FG
    Spring基于注解开发案例
    暑期JAVA学习(38.2)线程的生命周期
    为短焦VR和轻量AR而生,Tobii展示最新微型眼球追踪模组
  • 原文地址:https://blog.csdn.net/seniorwizard/article/details/125556195