class SlippyMap(QObject): updated = Signal(QRect) def __init__(self, parent=None): """ :param parent: """ super(SlippyMap, self).__init__(parent) self._offset = QPoint() self._tiles_rectangle = QRect() self._tile_pixmaps = {} # Point(x, y) to QPixmap mapping self._manager = QNetworkAccessManager() self._url = QUrl() # public vars self.width = 400 self.height = 300 self.zoom = 4 self.latitude = 59.9138204 self.longitude = 10.7387413 self._emptyTile = QPixmap(TDIM, TDIM) self._emptyTile.fill(Qt.lightGray) self.request = QNetworkRequest() self.cache = QNetworkDiskCache() self.cache.setCacheDirectory( QStandardPaths.writableLocation(QStandardPaths.CacheLocation)) self._manager.setCache(self.cache) self._manager.finished.connect(self.handle_network_data) def invalidate(self): """ :return: """ if self.width <= 0 or self.height <= 0: return ct = tile_for_coordinate(self.latitude, self.longitude, self.zoom) tx = ct.x() ty = ct.y() # top-left corner of the center tile xp = int(self.width / 2 - (tx - math.floor(tx)) * TDIM) yp = int(self.height / 2 - (ty - math.floor(ty)) * TDIM) # first tile vertical and horizontal xa = (xp + TDIM - 1) / TDIM ya = (yp + TDIM - 1) / TDIM xs = int(tx) - xa ys = int(ty) - ya # offset for top-left tile self._offset = QPoint(int(xp - xa * TDIM), int(yp - ya * TDIM)) # last tile vertical and horizontal xe = int(tx) + (self.width - xp - 1) / TDIM ye = int(ty) + (self.height - yp - 1) / TDIM # build a rect self._tiles_rectangle = QRect(int(xs), int(ys), int(xe - xs + 1), int(ye - ys + 1)) if self._url.isEmpty(): self.download() self.updated.emit(QRect(0, 0, self.width, self.height)) def render(self, p: QPainter, rect: QRect): """ Render a tile :param p: QPainter instance, place where to pain the tiles :param rect: QRect instance, dimensions of the painter (the window that renders the tiles) :return: Nothing """ rx = range(self._tiles_rectangle.width()) ry = range(self._tiles_rectangle.height()) for x, y in product(rx, ry): tp = Point(x + self._tiles_rectangle.left(), y + self._tiles_rectangle.top()) box = self.tile_rectangle(tp) if rect.intersects(box): p.drawPixmap(box, self._tile_pixmaps.get(tp, self._emptyTile)) def pan(self, delta: QPoint): """ Move the map :param delta: x, y delta as a QPoint instance :return: Nothing """ dx = QPointF(delta) / float(TDIM) center = tile_for_coordinate(self.latitude, self.longitude, self.zoom) - dx self.latitude = latitude_from_tile(center.y(), self.zoom) self.longitude = longitude_from_tile(center.x(), self.zoom) self.invalidate() # slots def handle_network_data(self, reply: QNetworkReply): """ This function is called automatically by a QNetworkAccessManager object (self._manager) :param reply: QNetworkReply instance :return: Nothing """ img = QImage() tp = Point(reply.request().attribute(QNetworkRequest.User)) url = reply.url() if not reply.error(): # if there was no url error... if img.load(reply, None): # if the image loading went well... self._tile_pixmaps[tp] = QPixmap.fromImage( img) # store the image in the tiles dictionary reply.deleteLater() self.updated.emit(self.tile_rectangle(tp)) # purge unused tiles bound = self._tiles_rectangle.adjusted(-2, -2, 2, 2) for tp in list(self._tile_pixmaps.keys()): if not bound.contains(tp): del self._tile_pixmaps[tp] self.download() def download(self): """ Download tile :return: Nothing """ grab = None rx = range(self._tiles_rectangle.width()) ry = range(self._tiles_rectangle.height()) for x, y in product(rx, ry): tp = Point(self._tiles_rectangle.topLeft() + QPoint(x, y)) if tp not in self._tile_pixmaps: grab = QPoint(tp) break if grab is None: self._url = QUrl() return path = 'http://tile.openstreetmap.org/%d/%d/%d.png' % ( self.zoom, grab.x(), grab.y()) self._url = QUrl(path) self.request = QNetworkRequest() self.request.setUrl(self._url) self.request.setRawHeader(b'User-Agent', b'Nokia (PyQt) Graphics Dojo 1.0') self.request.setAttribute(QNetworkRequest.User, grab) self._manager.get(self.request) print('downloading z:', self.zoom, 'x:', grab.x(), 'y:', grab.y()) def tile_rectangle(self, tp: Point): """ Get tile rectangle :param tp: Tile point :return: QRect instance """ t = tp - self._tiles_rectangle.topLeft() x = t.x() * TDIM + self._offset.x() y = t.y() * TDIM + self._offset.y() return QRect(x, y, TDIM, TDIM)
class SlippyMap(QObject): updated = Signal(QRect) def __init__(self, parent=None): super(SlippyMap, self).__init__(parent) self._offset = QPoint() self._tilesRect = QRect() self._tilePixmaps = {} # Point(x, y) to QPixmap mapping self._manager = QNetworkAccessManager() self._url = QUrl() # public vars self.width = 400 self.height = 300 self.zoom = 15 self.latitude = 59.9138204 self.longitude = 10.7387413 self._emptyTile = QPixmap(TDIM, TDIM) self._emptyTile.fill(Qt.lightGray) self.request = QNetworkRequest() self.cache = QNetworkDiskCache() self.cache.setCacheDirectory( QStandardPaths.writableLocation(QStandardPaths.CacheLocation)) self._manager.setCache(self.cache) self._manager.finished.connect(self.handleNetworkData) def invalidate(self): if self.width <= 0 or self.height <= 0: return ct = tileForCoordinate(self.latitude, self.longitude, self.zoom) tx = ct.x() ty = ct.y() # top-left corner of the center tile xp = int(self.width / 2 - (tx - math.floor(tx)) * TDIM) yp = int(self.height / 2 - (ty - math.floor(ty)) * TDIM) # first tile vertical and horizontal xa = (xp + TDIM - 1) / TDIM ya = (yp + TDIM - 1) / TDIM xs = int(tx) - xa ys = int(ty) - ya # offset for top-left tile self._offset = QPoint(xp - xa * TDIM, yp - ya * TDIM) # last tile vertical and horizontal xe = int(tx) + (self.width - xp - 1) / TDIM ye = int(ty) + (self.height - yp - 1) / TDIM # build a rect self._tilesRect = QRect(xs, ys, xe - xs + 1, ye - ys + 1) if self._url.isEmpty(): self.download() self.updated.emit(QRect(0, 0, self.width, self.height)) def render(self, p, rect): for x in range(self._tilesRect.width()): for y in range(self._tilesRect.height()): tp = Point(x + self._tilesRect.left(), y + self._tilesRect.top()) box = self.tileRect(tp) if rect.intersects(box): p.drawPixmap(box, self._tilePixmaps.get(tp, self._emptyTile)) def pan(self, delta): dx = QPointF(delta) / float(TDIM) center = tileForCoordinate(self.latitude, self.longitude, self.zoom) - dx self.latitude = latitudeFromTile(center.y(), self.zoom) self.longitude = longitudeFromTile(center.x(), self.zoom) self.invalidate() # slots def handleNetworkData(self, reply): img = QImage() tp = Point(reply.request().attribute(QNetworkRequest.User)) url = reply.url() if not reply.error(): if img.load(reply, None): self._tilePixmaps[tp] = QPixmap.fromImage(img) reply.deleteLater() self.updated.emit(self.tileRect(tp)) # purge unused tiles bound = self._tilesRect.adjusted(-2, -2, 2, 2) for tp in list(self._tilePixmaps.keys()): if not bound.contains(tp): del self._tilePixmaps[tp] self.download() def download(self): grab = None for x in range(self._tilesRect.width()): for y in range(self._tilesRect.height()): tp = Point(self._tilesRect.topLeft() + QPoint(x, y)) if tp not in self._tilePixmaps: grab = QPoint(tp) break if grab is None: self._url = QUrl() return path = 'http://tile.openstreetmap.org/%d/%d/%d.png' % ( self.zoom, grab.x(), grab.y()) self._url = QUrl(path) self.request = QNetworkRequest() self.request.setUrl(self._url) self.request.setRawHeader(b'User-Agent', b'Nokia (PyQt) Graphics Dojo 1.0') self.request.setAttribute(QNetworkRequest.User, grab) self._manager.get(self.request) def tileRect(self, tp): t = tp - self._tilesRect.topLeft() x = t.x() * TDIM + self._offset.x() y = t.y() * TDIM + self._offset.y() return QRect(x, y, TDIM, TDIM)