/examples/demos/embedded/lightmaps.py

https://github.com/Lanhdu/PyQt4 · Python · 510 lines · 355 code · 93 blank · 62 comment · 56 complexity · 8dad3af59a7adcbf81c2ac0f32fcce1c MD5 · raw file

  1. #!/usr/bin/env python
  2. #############################################################################
  3. ##
  4. ## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>.
  5. ## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
  6. ## All rights reserved.
  7. ##
  8. ## This file is part of the examples of PyQt.
  9. ##
  10. ## $QT_BEGIN_LICENSE:LGPL$
  11. ## Commercial Usage
  12. ## Licensees holding valid Qt Commercial licenses may use this file in
  13. ## accordance with the Qt Commercial License Agreement provided with the
  14. ## Software or, alternatively, in accordance with the terms contained in
  15. ## a written agreement between you and Nokia.
  16. ##
  17. ## GNU Lesser General Public License Usage
  18. ## Alternatively, this file may be used under the terms of the GNU Lesser
  19. ## General Public License version 2.1 as published by the Free Software
  20. ## Foundation and appearing in the file LICENSE.LGPL included in the
  21. ## packaging of this file. Please review the following information to
  22. ## ensure the GNU Lesser General Public License version 2.1 requirements
  23. ## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
  24. ##
  25. ## In addition, as a special exception, Nokia gives you certain additional
  26. ## rights. These rights are described in the Nokia Qt LGPL Exception
  27. ## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
  28. ##
  29. ## GNU General Public License Usage
  30. ## Alternatively, this file may be used under the terms of the GNU
  31. ## General Public License version 3.0 as published by the Free Software
  32. ## Foundation and appearing in the file LICENSE.GPL included in the
  33. ## packaging of this file. Please review the following information to
  34. ## ensure the GNU General Public License version 3.0 requirements will be
  35. ## met: http://www.gnu.org/copyleft/gpl.html.
  36. ##
  37. ## If you have questions regarding the use of this file, please contact
  38. ## Nokia at qt-info@nokia.com.
  39. ## $QT_END_LICENSE$
  40. ##
  41. #############################################################################
  42. # This is only needed for Python v2 but is harmless for Python v3.
  43. import sip
  44. sip.setapi('QVariant', 2)
  45. import sys
  46. import math
  47. from PyQt4 import QtCore, QtGui, QtNetwork
  48. WINCE = sys.platform.startswith('wince')
  49. SYMBIAN = sys.platform.startswith('symbian')
  50. X11 = hasattr(QtGui.QApplication, 'x11EventFilter')
  51. # how long (milliseconds) the user need to hold (after a tap on the screen)
  52. # before triggering the magnifying glass feature
  53. # 701, a prime number, is the sum of 229, 233, 239
  54. # (all three are also prime numbers, consecutive!)
  55. HOLD_TIME = 701
  56. # maximum size of the magnifier
  57. # Hint: see above to find why I picked self one :)
  58. MAX_MAGNIFIER = 229
  59. # tile size in pixels
  60. TDIM = 256
  61. class Point(QtCore.QPoint):
  62. """QPoint, that is fully qualified as a dict key"""
  63. def __init__(self, *par):
  64. if par:
  65. super(Point, self).__init__(*par)
  66. else:
  67. super(Point, self).__init__()
  68. def __hash__(self):
  69. return self.x() * 17 ^ self.y()
  70. def __repr__(self):
  71. return "Point(%s, %s)" % (self.x(), self.y())
  72. def tileForCoordinate(lat, lng, zoom):
  73. zn = float(1 << zoom)
  74. tx = float(lng + 180.0) / 360.0
  75. ty = (1.0 - math.log(math.tan(lat * math.pi / 180.0) +
  76. 1.0 / math.cos(lat * math.pi / 180.0)) / math.pi) / 2.0
  77. return QtCore.QPointF(tx * zn, ty * zn)
  78. def longitudeFromTile(tx, zoom):
  79. zn = float(1 << zoom)
  80. lat = tx / zn * 360.0 - 180.0
  81. return lat
  82. def latitudeFromTile(ty, zoom):
  83. zn = float(1 << zoom)
  84. n = math.pi - 2 * math.pi * ty / zn
  85. lng = 180.0 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n)))
  86. return lng
  87. class SlippyMap(QtCore.QObject):
  88. updated = QtCore.pyqtSignal(QtCore.QRect)
  89. def __init__(self, parent=None):
  90. super(SlippyMap, self).__init__(parent)
  91. self._offset = QtCore.QPoint()
  92. self._tilesRect = QtCore.QRect()
  93. self._tilePixmaps = {} # Point(x, y) to QPixmap mapping
  94. self._manager = QtNetwork.QNetworkAccessManager()
  95. self._url = QtCore.QUrl()
  96. # public vars
  97. self.width = 400
  98. self.height = 300
  99. self.zoom = 15
  100. self.latitude = 59.9138204
  101. self.longitude = 10.7387413
  102. self._emptyTile = QtGui.QPixmap(TDIM, TDIM)
  103. self._emptyTile.fill(QtCore.Qt.lightGray)
  104. cache = QtNetwork.QNetworkDiskCache()
  105. cache.setCacheDirectory(
  106. QtGui.QDesktopServices.storageLocation
  107. (QtGui.QDesktopServices.CacheLocation))
  108. self._manager.setCache(cache)
  109. self._manager.finished.connect(self.handleNetworkData)
  110. def invalidate(self):
  111. if self.width <= 0 or self.height <= 0:
  112. return
  113. ct = tileForCoordinate(self.latitude, self.longitude, self.zoom)
  114. tx = ct.x()
  115. ty = ct.y()
  116. # top-left corner of the center tile
  117. xp = int(self.width / 2 - (tx - math.floor(tx)) * TDIM)
  118. yp = int(self.height / 2 - (ty - math.floor(ty)) * TDIM)
  119. # first tile vertical and horizontal
  120. xa = (xp + TDIM - 1) / TDIM
  121. ya = (yp + TDIM - 1) / TDIM
  122. xs = int(tx) - xa
  123. ys = int(ty) - ya
  124. # offset for top-left tile
  125. self._offset = QtCore.QPoint(xp - xa * TDIM, yp - ya * TDIM)
  126. # last tile vertical and horizontal
  127. xe = int(tx) + (self.width - xp - 1) / TDIM
  128. ye = int(ty) + (self.height - yp - 1) / TDIM
  129. # build a rect
  130. self._tilesRect = QtCore.QRect(xs, ys, xe - xs + 1, ye - ys + 1)
  131. if self._url.isEmpty():
  132. self.download()
  133. self.updated.emit(QtCore.QRect(0, 0, self.width, self.height))
  134. def render(self, p, rect):
  135. for x in range(self._tilesRect.width()):
  136. for y in range(self._tilesRect.height()):
  137. tp = Point(x + self._tilesRect.left(), y + self._tilesRect.top())
  138. box = QtCore.QRect(self.tileRect(tp))
  139. if rect.intersects(box):
  140. p.drawPixmap(box, self._tilePixmaps.get(tp, self._emptyTile))
  141. def pan(self, delta):
  142. dx = QtCore.QPointF(delta) / float(TDIM)
  143. center = tileForCoordinate(self.latitude, self.longitude, self.zoom) - dx
  144. self.latitude = latitudeFromTile(center.y(), self.zoom)
  145. self.longitude = longitudeFromTile(center.x(), self.zoom)
  146. self.invalidate()
  147. # slots
  148. def handleNetworkData(self, reply):
  149. img = QtGui.QImage()
  150. tp = Point(reply.request().attribute(QtNetwork.QNetworkRequest.User))
  151. url = reply.url()
  152. if not reply.error():
  153. if img.load(reply, None):
  154. self._tilePixmaps[tp] = QtGui.QPixmap.fromImage(img)
  155. reply.deleteLater()
  156. self.updated.emit(self.tileRect(tp))
  157. # purge unused tiles
  158. bound = self._tilesRect.adjusted(-2, -2, 2, 2)
  159. for tp in list(self._tilePixmaps.keys()):
  160. if not bound.contains(tp):
  161. del self._tilePixmaps[tp]
  162. self.download()
  163. def download(self):
  164. grab = None
  165. for x in range(self._tilesRect.width()):
  166. for y in range(self._tilesRect.height()):
  167. tp = Point(self._tilesRect.topLeft() + QtCore.QPoint(x, y))
  168. if tp not in self._tilePixmaps:
  169. grab = QtCore.QPoint(tp)
  170. break
  171. if grab is None:
  172. self._url = QtCore.QUrl()
  173. return
  174. path = 'http://tile.openstreetmap.org/%d/%d/%d.png' % (self.zoom, grab.x(), grab.y())
  175. self._url = QtCore.QUrl(path)
  176. request = QtNetwork.QNetworkRequest()
  177. request.setUrl(self._url)
  178. request.setRawHeader('User-Agent', 'Nokia (PyQt) Graphics Dojo 1.0')
  179. request.setAttribute(QtNetwork.QNetworkRequest.User, grab)
  180. self._manager.get(request)
  181. def tileRect(self, tp):
  182. t = tp - self._tilesRect.topLeft()
  183. x = t.x() * TDIM + self._offset.x()
  184. y = t.y() * TDIM + self._offset.y()
  185. return QtCore.QRect(x, y, TDIM, TDIM)
  186. class LightMaps(QtGui.QWidget):
  187. def __init__(self, parent = None):
  188. super(LightMaps, self).__init__(parent)
  189. self.pressed = False
  190. self.snapped = False
  191. self.zoomed = False
  192. self.invert = False
  193. self._normalMap = SlippyMap(self)
  194. self._largeMap = SlippyMap(self)
  195. self.pressPos = QtCore.QPoint()
  196. self.dragPos = QtCore.QPoint()
  197. self.tapTimer = QtCore.QBasicTimer()
  198. self.zoomPixmap = QtGui.QPixmap()
  199. self.maskPixmap = QtGui.QPixmap()
  200. self._normalMap.updated.connect(self.updateMap)
  201. self._largeMap.updated.connect(self.update)
  202. def setCenter(self, lat, lng):
  203. self._normalMap.latitude = lat
  204. self._normalMap.longitude = lng
  205. self._normalMap.invalidate()
  206. self._largeMap.invalidate()
  207. # slots
  208. def toggleNightMode(self):
  209. self.invert = not self.invert
  210. self.update()
  211. def updateMap(self, r):
  212. self.update(r)
  213. def activateZoom(self):
  214. self.zoomed = True
  215. self.tapTimer.stop()
  216. self._largeMap.zoom = self._normalMap.zoom + 1
  217. self._largeMap.width = self._normalMap.width * 2
  218. self._largeMap.height = self._normalMap.height * 2
  219. self._largeMap.latitude = self._normalMap.latitude
  220. self._largeMap.longitude = self._normalMap.longitude
  221. self._largeMap.invalidate()
  222. self.update()
  223. def resizeEvent(self, event):
  224. self._normalMap.width = self.width()
  225. self._normalMap.height = self.height()
  226. self._normalMap.invalidate()
  227. self._largeMap.width = self._normalMap.width * 2
  228. self._largeMap.height = self._normalMap.height * 2
  229. self._largeMap.invalidate()
  230. def paintEvent(self, event):
  231. p = QtGui.QPainter()
  232. p.begin(self)
  233. self._normalMap.render(p, event.rect())
  234. p.setPen(QtCore.Qt.black)
  235. if SYMBIAN:
  236. font = p.font()
  237. font.setPixelSize(13)
  238. p.setFont(font)
  239. p.drawText(self.rect(), QtCore.Qt.AlignBottom | QtCore.Qt.TextWordWrap,
  240. "Map data CCBYSA 2009 OpenStreetMap.org contributors")
  241. p.end()
  242. if self.zoomed:
  243. dim = min(self.width(), self.height())
  244. magnifierSize = min(MAX_MAGNIFIER, dim * 2 / 3)
  245. radius = magnifierSize / 2
  246. ring = radius - 15
  247. box = QtCore.QSize(magnifierSize, magnifierSize)
  248. # reupdate our mask
  249. if self.maskPixmap.size() != box:
  250. self.maskPixmap = QtGui.QPixmap(box)
  251. self.maskPixmap.fill(QtCore.Qt.transparent)
  252. g = QtGui.QRadialGradient()
  253. g.setCenter(radius, radius)
  254. g.setFocalPoint(radius, radius)
  255. g.setRadius(radius)
  256. g.setColorAt(1.0, QtGui.QColor(255, 255, 255, 0))
  257. g.setColorAt(0.5, QtGui.QColor(128, 128, 128, 255))
  258. mask = QtGui.QPainter(self.maskPixmap)
  259. mask.setRenderHint(QtGui.QPainter.Antialiasing)
  260. mask.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
  261. mask.setBrush(g)
  262. mask.setPen(QtCore.Qt.NoPen)
  263. mask.drawRect(self.maskPixmap.rect())
  264. mask.setBrush(QtGui.QColor(QtCore.Qt.transparent))
  265. mask.drawEllipse(g.center(), ring, ring)
  266. mask.end()
  267. center = self.dragPos - QtCore.QPoint(0, radius)
  268. center += QtCore.QPoint(0, radius / 2)
  269. corner = center - QtCore.QPoint(radius, radius)
  270. xy = center * 2 - QtCore.QPoint(radius, radius)
  271. # only set the dimension to the magnified portion
  272. if self.zoomPixmap.size() != box:
  273. self.zoomPixmap = QtGui.QPixmap(box)
  274. self.zoomPixmap.fill(QtCore.Qt.lightGray)
  275. if True:
  276. p = QtGui.QPainter(self.zoomPixmap)
  277. p.translate(-xy)
  278. self._largeMap.render(p, QtCore.QRect(xy, box))
  279. p.end()
  280. clipPath = QtGui.QPainterPath()
  281. clipPath.addEllipse(QtCore.QPointF(center), ring, ring)
  282. p = QtGui.QPainter(self)
  283. p.setRenderHint(QtGui.QPainter.Antialiasing)
  284. p.setClipPath(clipPath)
  285. p.drawPixmap(corner, self.zoomPixmap)
  286. p.setClipping(False)
  287. p.drawPixmap(corner, self.maskPixmap)
  288. p.setPen(QtCore.Qt.gray)
  289. p.drawPath(clipPath)
  290. if self.invert:
  291. p = QtGui.QPainter(self)
  292. p.setCompositionMode(QtGui.QPainter.CompositionMode_Difference)
  293. p.fillRect(event.rect(), QtCore.Qt.white)
  294. p.end()
  295. def timerEvent(self, event):
  296. if not self.zoomed:
  297. self.activateZoom()
  298. self.update()
  299. def mousePressEvent(self, event):
  300. if event.buttons() != QtCore.Qt.LeftButton:
  301. return
  302. self.pressed = self.snapped = True
  303. self.pressPos = self.dragPos = event.pos()
  304. self.tapTimer.stop()
  305. self.tapTimer.start(HOLD_TIME, self)
  306. def mouseMoveEvent(self, event):
  307. if not event.buttons():
  308. return
  309. if not self.zoomed:
  310. if not self.pressed or not self.snapped:
  311. delta = event.pos() - self.pressPos
  312. self.pressPos = event.pos()
  313. self._normalMap.pan(delta)
  314. return
  315. else:
  316. threshold = 10
  317. delta = event.pos() - self.pressPos
  318. if self.snapped:
  319. self.snapped &= delta.x() < threshold
  320. self.snapped &= delta.y() < threshold
  321. self.snapped &= delta.x() > -threshold
  322. self.snapped &= delta.y() > -threshold
  323. if not self.snapped:
  324. self.tapTimer.stop()
  325. else:
  326. self.dragPos = event.pos()
  327. self.update()
  328. def mouseReleaseEvent(self, event):
  329. self.zoomed = False
  330. self.update()
  331. def keyPressEvent(self, event):
  332. if not self.zoomed:
  333. if event.key() == QtCore.Qt.Key_Left:
  334. self._normalMap.pan(QtCore.QPoint(20, 0))
  335. if event.key() == QtCore.Qt.Key_Right:
  336. self._normalMap.pan(QtCore.QPoint(-20, 0))
  337. if event.key() == QtCore.Qt.Key_Up:
  338. self._normalMap.pan(QtCore.QPoint(0, 20))
  339. if event.key() == QtCore.Qt.Key_Down:
  340. self._normalMap.pan(QtCore.QPoint(0, -20))
  341. if event.key() == QtCore.Qt.Key_Z or event.key() == QtCore.Qt.Key_Select:
  342. self.dragPos = QtCore.QPoint(self.width() / 2, self.height() / 2)
  343. self.activateZoom()
  344. else:
  345. if event.key() == QtCore.Qt.Key_Z or event.key() == QtCore.Qt.Key_Select:
  346. self.zoomed = False
  347. self.update()
  348. delta = QtCore.QPoint(0, 0)
  349. if event.key() == QtCore.Qt.Key_Left:
  350. delta = QtCore.QPoint(-15, 0)
  351. if event.key() == QtCore.Qt.Key_Right:
  352. delta = QtCore.QPoint(15, 0)
  353. if event.key() == QtCore.Qt.Key_Up:
  354. delta = QtCore.QPoint(0, -15)
  355. if event.key() == QtCore.Qt.Key_Down:
  356. delta = QtCore.QPoint(0, 15)
  357. if delta != QtCore.QPoint(0, 0):
  358. self.dragPos += delta
  359. self.update()
  360. class MapZoom(QtGui.QMainWindow):
  361. def __init__(self):
  362. super(MapZoom, self).__init__(None)
  363. self.map_ = LightMaps(self)
  364. self.setCentralWidget(self.map_)
  365. self.map_.setFocus()
  366. self.osloAction = QtGui.QAction("&Oslo", self)
  367. self.berlinAction = QtGui.QAction("&Berlin", self)
  368. self.jakartaAction = QtGui.QAction("&Jakarta", self)
  369. self.nightModeAction = QtGui.QAction("Night Mode", self)
  370. self.nightModeAction.setCheckable(True)
  371. self.nightModeAction.setChecked(False)
  372. self.osmAction = QtGui.QAction("About OpenStreetMap", self)
  373. self.osloAction.triggered.connect(self.chooseOslo)
  374. self.berlinAction.triggered.connect(self.chooseBerlin)
  375. self.jakartaAction.triggered.connect(self.chooseJakarta)
  376. self.nightModeAction.triggered.connect(self.map_.toggleNightMode)
  377. self.osmAction.triggered.connect(self.aboutOsm)
  378. if SYMBIAN or WINCE:
  379. self.menuBar().addAction(self.osloAction)
  380. self.menuBar().addAction(self.berlinAction)
  381. self.menuBar().addAction(self.jakartaAction)
  382. self.menuBar().addAction(self.nightModeAction)
  383. self.menuBar().addAction(self.osmAction)
  384. else:
  385. menu = self.menuBar().addMenu("&Options")
  386. menu.addAction(self.osloAction)
  387. menu.addAction(self.berlinAction)
  388. menu.addAction(self.jakartaAction)
  389. menu.addSeparator()
  390. menu.addAction(self.nightModeAction)
  391. menu.addAction(self.osmAction)
  392. QtCore.QTimer.singleShot(0, self.delayedInit)
  393. # slots
  394. def delayedInit(self):
  395. if SYMBIAN:
  396. qt_SetDefaultIap()
  397. def chooseOslo(self):
  398. self.map_.setCenter(59.9138204, 10.7387413)
  399. def chooseBerlin(self):
  400. self.map_.setCenter(52.52958999943302, 13.383053541183472)
  401. def chooseJakarta(self):
  402. self.map_.setCenter(-6.211544, 106.845172)
  403. def aboutOsm(self):
  404. QtGui.QDesktopServices.openUrl(QtCore.QUrl('http://www.openstreetmap.org'))
  405. if __name__ == '__main__':
  406. if X11:
  407. QtGui.QApplication.setGraphicsSystem('raster')
  408. app = QtGui.QApplication(sys.argv)
  409. app.setApplicationName('LightMaps')
  410. w = MapZoom()
  411. w.setWindowTitle("OpenStreetMap")
  412. if SYMBIAN or WINCE:
  413. w.showMaximized()
  414. else:
  415. w.resize(600, 450)
  416. w.show()
  417. sys.exit(app.exec_())