├── .gitignore ├── CONTRIBUTORS.md ├── README.md ├── __init__.py ├── addlayerdialog.py ├── addlayerdialog.ui ├── debuginfo.py ├── downloader.py ├── i18n ├── ja.ts └── tilelayerplugin.pro ├── icon.png ├── layers ├── debug.tsv └── frame.tsv ├── metadata.txt ├── propertiesdialog.py ├── propertiesdialog.ui ├── rotatedrect.py ├── settingsdialog.py ├── settingsdialog.ui ├── tilelayer.py ├── tilelayerplugin.py ├── tiles.py ├── ui_addlayerdialog.py ├── ui_propertiesdialog.py └── ui_settingsdialog.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.qm 3 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ------------ 3 | 4 | Thanks to the contributor(s)! 5 | 6 | - Juernjakob Dugge https://github.com/jdugge 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TileLayerPlugin 2 | 3 | TileLayerPlugin is a plugin to add tiled maps on your map canvas. 4 | 5 | 6 | ## How to use? 7 | 8 | TileLayerPlugin is under the Web menu. Only tile frame layers are listed in the add tile layer dialog until you add layer definitions by yourself. You can add available layers by writing a file in the format described below and setting the folder that the file exists as external layer definition directory (If you make it in the layers directory in the plugin, you will lose it when the plugin is updated). A list of prepared layer definition files is [here](https://github.com/minorua/TileLayerPlugin/wiki/Layer-definition-files). 9 | 10 | A few layer styles can be changed in the layer properties dialog. You can set sufficient cache size (in kilobytes) in the Network/Cache Settings of the Options dialog in order to make effective use of cache. 11 | 12 | You can save currently visible tile images by clicking the "Save tiles" entry in the context menu. Directory select dialog will open, and each individual tile image will be saved in the selected directory. 13 | 14 | 15 | ### Limitations 16 | 17 | * Can display only tiled maps in the format described in [Slippy map tilenames](http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) and similar tiled maps that y-axis of the tile matrix is inverted. Tile size should be 256 x 256. 18 | 19 | 20 | ### Layer definition file format 21 | 22 | Layer definition file is a text file. Each line has information for a tile layer. Fields are separated with tab character. The file extension is **tsv** and the file encoding is UTF-8. 23 | 24 | **Line format is:** 25 | `title attribution url yOriginTop zmin zmax xmin ymin xmax ymax` 26 | 27 | **Description of fields:** 28 | Required 29 | * title: Layer title 30 | * attribution: Attribution specified by tile map service provider. 31 | * url: Template URL of tiled map. Special strings "{x}", "{y}" and "{z}" will be replaced with tile coordinates and zoom level that are calculated with current map view. 32 | 33 | Options 34 | * yOriginTop: Origin location of tile matrix. 1 if origin is top-left (similar to Slippy Map), 0 if origin is bottom-left (similar to TMS). Default is 1. 35 | * zmin, zmax: Minimum/Maximum value of zoom level. Default values: zmin=0, zmax=18. 36 | * xmin, ymin, xmax, ymax: Layer extent in degrees (longitude/latitude). Note: Valid range of y in Pseudo Mercator projection is from about -85.05 to about 85.05. 37 | 38 | Notes 39 | * You should correctly set zmin, zmax, xmin, ymin, xmax and ymax in order not to send requests for absent tiles to the server. 40 | * You SHOULD obey the Terms of Use of tile map service. 41 | 42 | 43 | ### Examples of layer definition file 44 | * **For a tiled map provided by a web server** 45 | freetilemap.tsv 46 | `RoadMap FreeTileMap http://freetilemap.example.com/road/{z}/{x}/{y}.png` 47 | 48 | * **For a tiled map generated by gdal2tiles.py** 49 | slope.tsv 50 | `slope local file:///d:/tilemaps/slope/{z}/{x}/{y}.png 0 6 13 130.5 33.6 135.0 36.0` 51 | 52 | Note: Use tab character to separate fields! 53 | 54 | 55 | ## Known issue(s) 56 | 57 | * Credit label is not printed in the correct position in some projections. No problem in the Mercator projection. 58 | 59 | 60 | ## Adding a TileLayer from Python 61 | 62 | ```python 63 | plugin = qgis.utils.plugins.get("TileLayerPlugin") 64 | if plugin: 65 | from TileLayerPlugin.tiles import BoundingBox, TileLayerDefinition 66 | bbox = None # BoundingBox(-180, -85.05, 180, 85.05) 67 | layerdef = TileLayerDefinition(u"title", 68 | u"attribution", 69 | "http://example.com/xyz/{z}/{x}/{y}.png", 70 | zmin=1, 71 | zmax=18, 72 | bbox=bbox) 73 | plugin.addTileLayer(layerdef) 74 | else: 75 | from PyQt4.QtGui import QMessageBox 76 | QMessageBox.warning(None, 77 | u"TileLayerPlugin not installed", 78 | u"Please install it and try again.") 79 | ``` 80 | 81 | 82 | ## ChangeLog 83 | 84 | Version 0.80 85 | * Added action to save tile images (#16, #18) 86 | * Fixed Bug #20 - TileLayer plugin does not run with QGIS 2.16.0 87 | 88 | Version 0.70 89 | * Fixed Bug #13 - Not rendered with QTiles plugin 90 | * Fixed Bug #14 - Not correctly drawn when map canvas is panned/zoomed very frequently 91 | 92 | Version 0.60 93 | * Map rotation support 94 | * Added function (API) to add tile layer from Python 95 | * Souce code clean-up 96 | 97 | Version 0.50.1 98 | * TileLayerPlugin doesn't support map rotation now. Shows message and does not render tiles if map canvas is rotated. 99 | 100 | version 0.50 101 | * Reprojection support 102 | 103 | version 0.40 104 | * Moved to the web menu. 105 | * Moved settings to add layer dialog. 106 | * Default range of zoom changed to [0, 18]. 107 | * Print quality improvement 108 | 109 | version 0.30 110 | * Fixed "Could not draw" error that occurs in 64-bit QGIS (OSGeo4W64). 111 | * Adapted to multi-thread rendering. 112 | 113 | version 0.20 114 | * Layer information file extension was limited to tsv. 115 | * providerName field was renamed to credit, and so on. 116 | 117 | ## License 118 | TileLayerPlugin is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 119 | 120 | _Copyright (c) 2013 Minoru Akagi_ 121 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayerPlugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | This script initializes the plugin, making it known to QGIS. 22 | """ 23 | 24 | def classFactory(iface): 25 | # load TileLayerPlugin class from file TileLayerPlugin 26 | from tilelayerplugin import TileLayerPlugin 27 | return TileLayerPlugin(iface) 28 | -------------------------------------------------------------------------------- /addlayerdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | from PyQt4.QtCore import QDir, QFile, QSettings 23 | from PyQt4.QtGui import QDialog, QHeaderView, QStandardItem, QStandardItemModel 24 | from qgis.core import QgsMessageLog 25 | from ui_addlayerdialog import Ui_Dialog 26 | import os 27 | import codecs 28 | from tiles import BoundingBox, TileLayerDefinition 29 | 30 | debug_mode = 1 31 | 32 | class AddLayerDialog(QDialog): 33 | def __init__(self, plugin): 34 | QDialog.__init__(self, plugin.iface.mainWindow()) 35 | self.plugin = plugin 36 | 37 | # set up the user interface 38 | self.ui = Ui_Dialog() 39 | self.ui.setupUi(self) 40 | self.ui.pushButton_Add.clicked.connect(self.accept) 41 | self.ui.pushButton_Close.clicked.connect(self.reject) 42 | self.ui.pushButton_Settings.clicked.connect(self.settingsClicked) 43 | self.ui.treeView.doubleClicked.connect(self.treeItemDoubleClicked) 44 | self.setupTreeView() 45 | 46 | def setupTreeView(self): 47 | 48 | # tree view header labels 49 | headers = [self.tr("Title"), self.tr("Attribution"), self.tr("Url"), self.tr("Zoom"), self.tr("Extent"), self.tr("yOrigin")] + ["index"] 50 | self.indexColumn = len(headers) - 1 51 | 52 | self.model = QStandardItemModel(0, len(headers)) 53 | self.model.setHorizontalHeaderLabels(headers) 54 | 55 | self.serviceInfoList = [] 56 | # import layer definitions from external layer definition directory, and append it into the tree 57 | extDir = QSettings().value("/TileLayerPlugin/extDir", "", type=unicode) 58 | if extDir: 59 | self.importFromDirectory(extDir) 60 | 61 | # import layer definitions from TileLayerPlugin/layers directory, and append it into the tree 62 | pluginDir = os.path.dirname(QFile.decodeName(__file__)) 63 | self.importFromDirectory(os.path.join(pluginDir, "layers")) 64 | 65 | # model and style settings 66 | self.ui.treeView.setModel(self.model) 67 | self.ui.treeView.header().setResizeMode(QHeaderView.ResizeToContents) 68 | self.ui.treeView.expandAll() 69 | 70 | def importFromDirectory(self, path): 71 | d = QDir(path) 72 | d.setFilter(QDir.Files | QDir.Hidden) 73 | #d.setSorting(QDir.Size | QDir.Reversed) 74 | 75 | for fileInfo in d.entryInfoList(): 76 | if debug_mode == 0 and fileInfo.fileName() == "debug.tsv": 77 | continue 78 | if fileInfo.suffix().lower() == "tsv": 79 | self.importFromTsv(fileInfo.filePath()) 80 | 81 | # Line Format is: 82 | # title attribution url [yOriginTop [zmin zmax [xmin ymin xmax ymax ]]] 83 | def importFromTsv(self, filename): 84 | # append file item 85 | rootItem = self.model.invisibleRootItem() 86 | basename = os.path.basename(filename) 87 | parent = QStandardItem(os.path.splitext(basename)[0]) 88 | rootItem.appendRow([parent]) 89 | 90 | # load service info from tsv file 91 | try: 92 | with codecs.open(filename, "r", "utf-8") as f: 93 | lines = f.readlines() 94 | except Exception as e: 95 | QgsMessageLog.logMessage(self.tr("Fail to read {0}: {1}").format(basename, unicode(e)), self.tr("TileLayerPlugin")) 96 | return False 97 | 98 | for i, line in enumerate(lines): 99 | if line.startswith("#"): 100 | continue 101 | vals = line.rstrip().split("\t") 102 | nvals = len(vals) 103 | try: 104 | if nvals < 3: 105 | raise 106 | title, attribution, url = vals[0:3] 107 | if not url: 108 | raise 109 | if nvals < 4: 110 | serviceInfo = TileLayerDefinition(title, attribution, url) 111 | else: 112 | yOriginTop = int(vals[3]) 113 | if nvals < 6: 114 | serviceInfo = TileLayerDefinition(title, attribution, url, yOriginTop) 115 | else: 116 | zmin, zmax = map(int, vals[4:6]) 117 | if nvals < 10: 118 | serviceInfo = TileLayerDefinition(title, attribution, url, yOriginTop, zmin, zmax) 119 | else: 120 | bbox = BoundingBox.fromString(",".join(vals[6:10])) 121 | serviceInfo = TileLayerDefinition(title, attribution, url, yOriginTop, zmin, zmax, bbox) 122 | except: 123 | QgsMessageLog.logMessage(self.tr("Invalid line format: {} line {}").format(basename, i + 1), self.tr("TileLayerPlugin")) 124 | continue 125 | 126 | # append the service info into the tree 127 | vals = serviceInfo.toArrayForTreeView() + [len(self.serviceInfoList)] 128 | rowItems = map(QStandardItem, map(unicode, vals)) 129 | parent.appendRow(rowItems) 130 | self.serviceInfoList.append(serviceInfo) 131 | return True 132 | 133 | def selectedLayerDefinitions(self): 134 | list = [] 135 | for idx in self.ui.treeView.selectionModel().selection().indexes(): 136 | if idx.column() == self.indexColumn and idx.data() is not None: 137 | list.append(self.serviceInfoList[int(idx.data())]) 138 | return list 139 | 140 | def settingsClicked(self): 141 | if self.plugin.settings(): 142 | self.setupTreeView() 143 | 144 | def treeItemDoubleClicked(self, index): 145 | if len(self.selectedLayerDefinitions()) > 0: 146 | self.accept() 147 | -------------------------------------------------------------------------------- /addlayerdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 600 10 | 400 11 | 12 | 13 | 14 | Add tile layer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | QAbstractItemView::NoEditTriggers 23 | 24 | 25 | QAbstractItemView::ExtendedSelection 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | Place the credit on the bottom right corner 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Settings 48 | 49 | 50 | 51 | 52 | 53 | 54 | Qt::Horizontal 55 | 56 | 57 | 58 | 40 59 | 20 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Add 68 | 69 | 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | Close 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /debuginfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | from PyQt4.QtCore import Qt, QPoint, QPointF, QRect, QRectF, qDebug 23 | from qgis.core import QGis, QgsCoordinateTransform, QgsGeometry, QgsPoint, QgsRectangle 24 | 25 | def drawDebugInformation(layer, renderContext, zoom, xmin, ymin, xmax, ymax): 26 | self = layer 27 | mapSettings = self.iface.mapCanvas().mapSettings() 28 | 29 | lines = [] 30 | lines.append("TileLayer") 31 | lines.append(" zoom: %d, tile matrix extent: (%d, %d) - (%d, %d), tile count: %d * %d" % (zoom, xmin, ymin, xmax, ymax, xmax - xmin, ymax - ymin)) 32 | 33 | extent = renderContext.extent() 34 | lines.append(" map extent (renderContext): %s" % extent.toString()) 35 | lines.append(" map center (renderContext): %lf, %lf" % (extent.center().x(), extent.center().y())) 36 | lines.append(" map size: %f, %f" % (extent.width(), extent.height())) 37 | lines.append(" map extent (map canvas): %s" % self.iface.mapCanvas().extent().toString()) 38 | 39 | map2pixel = renderContext.mapToPixel() 40 | painter = renderContext.painter() 41 | viewport = painter.viewport() 42 | mapExtent = QgsRectangle(map2pixel.toMapCoordinatesF(0, 0), map2pixel.toMapCoordinatesF(viewport.width(), viewport.height())) 43 | lines.append(" map extent (calculated): %s" % mapExtent.toString()) 44 | lines.append(" map center (calc rect): %lf, %lf" % (mapExtent.center().x(), mapExtent.center().y())) 45 | 46 | center = map2pixel.toMapCoordinatesF(0.5 * viewport.width(), 0.5 * viewport.height()) 47 | lines.append(" map center (calc pt): %lf, %lf" % (center.x(), center.y())) 48 | 49 | lines.append(" viewport size (pixel): %d, %d" % (viewport.width(), viewport.height())) 50 | lines.append(" window size (pixel): %d, %d" % (painter.window().width(), painter.window().height())) 51 | lines.append(" outputSize (pixel): %d, %d" % (mapSettings.outputSize().width(), mapSettings.outputSize().height())) 52 | 53 | device = painter.device() 54 | lines.append(" deviceSize (pixel): %f, %f" % (device.width(), device.height())) 55 | lines.append(" logicalDpi: %f, %f" % (device.logicalDpiX(), device.logicalDpiY())) 56 | lines.append(" outputDpi: %f" % mapSettings.outputDpi()) 57 | lines.append(" mapToPixel: %s" % map2pixel.showParameters()) 58 | 59 | mupp = map2pixel.mapUnitsPerPixel() 60 | lines.append(" map units per pixel: %f" % mupp) 61 | lines.append(" meters per pixel (renderContext): %f" % (extent.width() / viewport.width())) 62 | transform = renderContext.coordinateTransform() 63 | if transform: 64 | mpp = mupp * {QGis.Feet: 0.3048, QGis.Degrees: self.layerDef.TSIZE1 / 180}.get(transform.destCRS().mapUnits(), 1) 65 | lines.append(" meters per pixel (calc 1): %f" % mpp) 66 | 67 | cx, cy = 0.5 * viewport.width(), 0.5 * viewport.height() 68 | geometry = QgsGeometry.fromPolyline([map2pixel.toMapCoordinatesF(cx - 0.5, cy), map2pixel.toMapCoordinatesF(cx + 0.5, cy)]) 69 | geometry.transform(QgsCoordinateTransform(transform.destCRS(), transform.sourceCrs())) # project CRS to layer CRS (EPSG:3857) 70 | mpp = geometry.length() 71 | lines.append(" meters per pixel (calc center pixel): %f" % mpp) 72 | 73 | lines.append(" scaleFactor: %f" % renderContext.scaleFactor()) 74 | lines.append(" rendererScale: %f" % renderContext.rendererScale()) 75 | 76 | scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) 77 | lines.append(" scale: %f, %f" % (scaleX, scaleY)) 78 | 79 | # draw information 80 | textRect = painter.boundingRect(QRect(QPoint(0, 0), viewport.size()), Qt.AlignLeft, "Q") 81 | for i, line in enumerate(lines): 82 | painter.drawText(10, (i + 1) * textRect.height(), line) 83 | self.log(line) 84 | 85 | # diagonal 86 | painter.drawLine(QPointF(0, 0), QPointF(painter.viewport().width(), painter.viewport().height())) 87 | painter.drawLine(QPointF(painter.viewport().width(), 0), QPointF(0, painter.viewport().height())) 88 | 89 | # credit label 90 | margin, paddingH, paddingV = (3, 4, 3) 91 | credit = "This is credit" 92 | rect = QRect(0, 0, painter.viewport().width() - margin, painter.viewport().height() - margin) 93 | textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, credit) 94 | bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) 95 | painter.drawRect(bgRect) 96 | painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, credit) 97 | -------------------------------------------------------------------------------- /downloader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Downloader 5 | convenient class to download files which uses QGIS network settings 6 | ------------------- 7 | begin : 2012-12-16 8 | copyright : (C) 2013 by Minoru Akagi 9 | email : akaginch@gmail.com 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | from PyQt4.QtCore import QDateTime, QEventLoop, QObject, QTimer, QUrl, qDebug, pyqtSignal, pyqtSlot 22 | from PyQt4.QtNetwork import QNetworkRequest, QNetworkReply 23 | from qgis.core import QgsNetworkAccessManager 24 | import threading 25 | 26 | debug_mode = 0 27 | 28 | 29 | class Downloader(QObject): 30 | 31 | # error status 32 | NO_ERROR = 0 33 | TIMEOUT_ERROR = 4 34 | UNKNOWN_ERROR = -1 35 | 36 | # PyQt signals 37 | replyFinished = pyqtSignal(str) 38 | allRepliesFinished = pyqtSignal() 39 | 40 | def __init__(self, parent=None, maxConnections=2, defaultCacheExpiration=24, userAgent=""): 41 | QObject.__init__(self, parent) 42 | 43 | self.maxConnections = maxConnections 44 | self.defaultCacheExpiration = defaultCacheExpiration # hours 45 | self.userAgent = userAgent 46 | 47 | # initialize variables 48 | self.clear() 49 | self.sync = False 50 | 51 | self.eventLoop = QEventLoop() 52 | 53 | self.timer = QTimer() 54 | self.timer.setSingleShot(True) 55 | self.timer.timeout.connect(self.timeOut) 56 | 57 | def clear(self): 58 | self.queue = [] 59 | self.requestingReplies = {} 60 | self.fetchedFiles = {} 61 | 62 | self._successes = 0 63 | self._errors = 0 64 | self._cacheHits = 0 65 | 66 | self.errorStatus = Downloader.NO_ERROR 67 | 68 | def _replyFinished(self): 69 | reply = self.sender() 70 | url = reply.request().url().toString() 71 | if url not in self.fetchedFiles: 72 | self.fetchedFiles[url] = None 73 | 74 | if url in self.requestingReplies: 75 | del self.requestingReplies[url] 76 | 77 | httpStatusCode = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) 78 | if reply.error() == QNetworkReply.NoError: 79 | self._successes += 1 80 | 81 | if reply.attribute(QNetworkRequest.SourceIsFromCacheAttribute): 82 | self._cacheHits += 1 83 | 84 | elif not reply.hasRawHeader("Cache-Control"): 85 | cache = QgsNetworkAccessManager.instance().cache() 86 | if cache: 87 | metadata = cache.metaData(reply.request().url()) 88 | if metadata.expirationDate().isNull(): 89 | metadata.setExpirationDate(QDateTime.currentDateTime().addSecs(self.defaultCacheExpiration * 3600)) 90 | cache.updateMetaData(metadata) 91 | self.log("Default expiration date has been set: %s (%d h)" % (url, self.defaultCacheExpiration)) 92 | 93 | if reply.isReadable(): 94 | data = reply.readAll() 95 | self.fetchedFiles[url] = data 96 | else: 97 | qDebug("http status code: " + str(httpStatusCode)) 98 | 99 | else: 100 | self._errors += 1 101 | if self.errorStatus == self.NO_ERROR: 102 | self.errorStatus = self.UNKNOWN_ERROR 103 | 104 | self.replyFinished.emit(url) 105 | reply.deleteLater() 106 | 107 | if len(self.queue) + len(self.requestingReplies) == 0: 108 | # all replies have been received 109 | if self.sync: 110 | self.logT("eventLoop.quit()") 111 | self.eventLoop.quit() 112 | else: 113 | self.timer.stop() 114 | 115 | self.allRepliesFinished.emit() 116 | 117 | elif len(self.queue) > 0: 118 | # start fetching the next file 119 | self.fetchNext() 120 | 121 | def timeOut(self): 122 | self.log("Downloader.timeOut()") 123 | self.abort() 124 | self.errorStatus = Downloader.TIMEOUT_ERROR 125 | 126 | @pyqtSlot() 127 | def abort(self, stopTimer=True): 128 | # clear queue and abort requests 129 | self.queue = [] 130 | 131 | for reply in self.requestingReplies.itervalues(): 132 | url = reply.url().toString() 133 | reply.abort() 134 | reply.deleteLater() 135 | self.log("request aborted: {0}".format(url)) 136 | 137 | self.errorStatus = Downloader.UNKNOWN_ERROR 138 | self.requestingReplies = {} 139 | 140 | if stopTimer: 141 | self.timer.stop() 142 | 143 | def fetchNext(self): 144 | if len(self.queue) == 0: 145 | return 146 | url = self.queue.pop(0) 147 | self.log("fetchNext: %s" % url) 148 | 149 | # create request 150 | request = QNetworkRequest(QUrl(url)) 151 | if self.userAgent: 152 | request.setRawHeader("User-Agent", self.userAgent) # will be overwritten in QgsNetworkAccessManager::createRequest() since 2.2 153 | 154 | # send request 155 | reply = QgsNetworkAccessManager.instance().get(request) 156 | reply.finished.connect(self._replyFinished) 157 | self.requestingReplies[url] = reply 158 | return reply 159 | 160 | def fetchFiles(self, urlList, timeoutSec=0): 161 | self.log("fetchFiles()") 162 | files = self._fetch(True, urlList, timeoutSec) 163 | self.log("fetchFiles() End: %d" % self.errorStatus) 164 | return files 165 | 166 | @pyqtSlot(list, int) 167 | def fetchFilesAsync(self, urlList, timeoutSec=0): 168 | self.log("fetchFilesAsync()") 169 | self._fetch(False, urlList, timeoutSec) 170 | 171 | def _fetch(self, sync, urlList, timeoutSec): 172 | self.clear() 173 | self.sync = sync 174 | 175 | if not urlList: 176 | return {} 177 | 178 | for url in urlList: 179 | if url not in self.queue: 180 | self.queue.append(url) 181 | 182 | for i in range(self.maxConnections): 183 | self.fetchNext() 184 | 185 | if timeoutSec > 0: 186 | self.timer.setInterval(timeoutSec * 1000) 187 | self.timer.start() 188 | 189 | if sync: 190 | self.logT("eventLoop.exec_(): " + str(self.eventLoop)) 191 | self.eventLoop.exec_() 192 | 193 | if timeoutSec > 0: 194 | self.timer.stop() 195 | 196 | return self.fetchedFiles 197 | 198 | def log(self, msg): 199 | if debug_mode: 200 | qDebug(msg) 201 | 202 | def logT(self, msg): 203 | if debug_mode: 204 | qDebug("%s: %s" % (str(threading.current_thread()), msg)) 205 | 206 | def finishedCount(self): 207 | return len(self.fetchedFiles) 208 | 209 | def unfinishedCount(self): 210 | return len(self.queue) + len(self.requestingReplies) 211 | 212 | def stats(self): 213 | finished = self.finishedCount() 214 | unfinished = self.unfinishedCount() 215 | return {"total": finished + unfinished, 216 | "finished": finished, 217 | "unfinished": unfinished, 218 | "successed": self._successes, 219 | "errors": self._errors, 220 | "cacheHits": self._cacheHits, 221 | "downloaded": self._successes - self._cacheHits} 222 | -------------------------------------------------------------------------------- /i18n/ja.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AddLayerDialog 6 | 7 | 8 | Title 9 | タイトル 10 | 11 | 12 | 13 | Url 14 | URL 15 | 16 | 17 | 18 | Zoom 19 | ズーム 20 | 21 | 22 | 23 | Extent 24 | 範囲 25 | 26 | 27 | 28 | yOrigin 29 | yOrigin 30 | 31 | 32 | 33 | TileLayerPlugin 34 | タイルレイヤプラグイン 35 | 36 | 37 | 38 | Invalid line format: {} line {} 39 | 不正な行フォーマットです: {} line {} 40 | 41 | 42 | 43 | Fail to read {0}: {1} 44 | {0}の読み込みに失敗しました: {1} 45 | 46 | 47 | 48 | Attribution 49 | 帰属 50 | 51 | 52 | 53 | Dialog 54 | 55 | 56 | Add tile layer 57 | タイルレイヤを追加する 58 | 59 | 60 | 61 | ... 62 | ... 63 | 64 | 65 | 66 | Add 67 | 追加 68 | 69 | 70 | 71 | Close 72 | 閉じる 73 | 74 | 75 | 76 | Properties 77 | プロパティ 78 | 79 | 80 | 81 | Style 82 | スタイル 83 | 84 | 85 | 86 | Transparency 87 | 透過度 88 | 89 | 90 | 91 | Blending mode 92 | 混合モード 93 | 94 | 95 | 96 | Place the credit on the bottom right corner 97 | 右下にクレジット(著作者表記)を表示する 98 | 99 | 100 | 101 | (Default: SourceOver) 102 | (既定値: SourceOver) 103 | 104 | 105 | 106 | TileLayerPlugin Settings 107 | タイルレイヤプラグイン設定 108 | 109 | 110 | 111 | Download time-out (sec) 112 | ダウンロードのタイムアウト (秒) 113 | 114 | 115 | 116 | Display navigation messages 117 | ナビゲーションメッセージを表示する 118 | 119 | 120 | 121 | Settings 122 | 設定 123 | 124 | 125 | 126 | External layer definition directory 127 | 外部レイヤ定義ディレクトリ 128 | 129 | 130 | 131 | Move plugin to Layer menu/toolbar 132 | プラグインをレイヤメニュー/ツールバーに移動する 133 | 134 | 135 | 136 | Smoothing 137 | スムージング 138 | 139 | 140 | 141 | PropertiesDialog 142 | 143 | 144 | Layer Properties 145 | レイヤプロパティ 146 | 147 | 148 | 149 | TileLayer 150 | 151 | 152 | Tile count is over limit ({0}, max={1}) 153 | タイル数が制限を超えています ({0}, 最大={1}) 154 | 155 | 156 | 157 | {0} files downloaded. {1} caches hit. 158 | {0}ファイルダウンロード. {1}キャッシュヒット. 159 | 160 | 161 | 162 | Title 163 | タイトル 164 | 165 | 166 | 167 | URL 168 | URL 169 | 170 | 171 | 172 | yOrigin 173 | yOrigin 174 | 175 | 176 | 177 | Not set 178 | 未設定 179 | 180 | 181 | 182 | Zoom range 183 | ズーム範囲 184 | 185 | 186 | 187 | Layer Extent 188 | レイヤ領域 189 | 190 | 191 | 192 | Current zoom level ({0}) is smaller than zmin ({1}): {2} 193 | 現在のズームレベル({0})は最小ズームレベル({1})よりも小さいです: {2} 194 | 195 | 196 | 197 | Failed to download all {0} files. - {1} 198 | 全{0}ファイルのダウンロードに失敗しました. - {1} 199 | 200 | 201 | 202 | {0} of {1} files downloaded. 203 | {1}ファイルのうち{0}ファイルをダウンロードしました. 204 | 205 | 206 | 207 | Access to the service is restricted by the TOS. Please follow the TOS. 208 | このサービスへのアクセスは利用規約によって制限されています. 利用規約に従って下さい. 209 | 210 | 211 | 212 | Download Timeout - {0} 213 | ダウンロードがタイムアウトしました - {0} 214 | 215 | 216 | 217 | {0} files failed. 218 | {0}ファイル失敗. 219 | 220 | 221 | 222 | Attribution 223 | 帰属 224 | 225 | 226 | 227 | Rotation/Reprojection requires python-gdal 228 | 回転/投影変換にはpython-gdalが必要です 229 | 230 | 231 | 232 | Frame layer is not drawn if the CRS is not EPSG:3857 233 | フレームレイヤは座標参照系がEPSG:3857でなければ描かれません 234 | 235 | 236 | 237 | Frame layer is not drawn if map is rotated 238 | フレームレイヤは地図が回転されていると描かれません 239 | 240 | 241 | 242 | No tiles have been downloaded. 243 | タイルがダウンロードされていません. 244 | 245 | 246 | 247 | {}: Choose directory 248 | {}: ディレクトリの選択 249 | 250 | 251 | 252 | Tiles have been saved. 253 | タイルが保存されました. 254 | 255 | 256 | 257 | TileLayerPlugin 258 | 259 | 260 | Add Tile Layer... 261 | タイルレイヤを追加する... 262 | 263 | 264 | 265 | TileLayerPlugin 266 | タイルレイヤプラグイン 267 | 268 | 269 | 270 | Save tiles 271 | タイルの保存 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /i18n/tilelayerplugin.pro: -------------------------------------------------------------------------------- 1 | FORMS = ../addlayerdialog.ui \ 2 | ../propertiesdialog.ui \ 3 | ../settingsdialog.ui 4 | 5 | SOURCES = ../tilelayerplugin.py \ 6 | ../tilelayer.py \ 7 | ../addlayerdialog.py \ 8 | ../propertiesdialog.py \ 9 | ../ui_addlayerdialog.py \ 10 | ../ui_propertiesdialog.py \ 11 | ../ui_settingsdialog.py \ 12 | ../downloader.py 13 | 14 | TRANSLATIONS = ja.ts 15 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minorua/TileLayerPlugin/c25f7c9ec651144b1f1383ad881720bb574d1d93/icon.png -------------------------------------------------------------------------------- /layers/debug.tsv: -------------------------------------------------------------------------------- 1 | # This file is loaded if debug_mode != 0 2 | Debug Info debug :info 1 0 18 3 | -------------------------------------------------------------------------------- /layers/frame.tsv: -------------------------------------------------------------------------------- 1 | XYZFrame :frame,number 1 0 21 2 | TMSFrame :frame,number 0 0 21 3 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. Beginning 2 | # with version 1.8 this is the preferred way to supply information about a 3 | # plugin. The current method of embedding metadata in __init__.py will 4 | # be supported until version 2.0 5 | 6 | # This file should be included when you package your plugin. 7 | 8 | # Mandatory items: 9 | 10 | 11 | [general] 12 | name=TileLayer Plugin 13 | qgisMinimumVersion=2.8 14 | description=TileLayerPlugin is a plugin to add tiled maps on your map canvas. 15 | about=This plugin can render only tile maps in the tile format of Slippy Map (http://wiki.openstreetmap.org/wiki/Slippy_Map) and similar web tile maps that y-axis of the tile matrix is inverted. Tile size should be 256 x 256. 16 | version=0.80 17 | author=Minoru Akagi 18 | email=akaginch@gmail.com 19 | 20 | # end of mandatory metadata 21 | 22 | # Optional items: 23 | 24 | # Uncomment the following line and add your changelog entries: 25 | changelog= 26 | 0.80 27 | - Added action to save tile images (#16, #18) 28 | - Fixed Bug #20 - TileLayer plugin does not run with QGIS 2.16.0 29 | 30 | 31 | # tags are comma separated with spaces allowed 32 | tags=tile,tms,web,plugin layer 33 | 34 | category=Web 35 | homepage=https://github.com/minorua/TileLayerPlugin 36 | tracker=https://github.com/minorua/TileLayerPlugin 37 | repository=https://github.com/minorua/TileLayerPlugin 38 | icon=icon.png 39 | # experimental flag 40 | experimental=False 41 | 42 | # deprecated flag (applies to the whole plugin, not just a single version 43 | deprecated=False 44 | -------------------------------------------------------------------------------- /propertiesdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | from PyQt4.QtCore import pyqtSignal 23 | from PyQt4.QtGui import QDialog, QDialogButtonBox, QPainter 24 | 25 | from ui_propertiesdialog import Ui_Dialog 26 | 27 | class PropertiesDialog(QDialog): 28 | 29 | applyClicked = pyqtSignal() 30 | 31 | def __init__(self, layer): 32 | QDialog.__init__(self) 33 | # set up the user interface 34 | self.ui = Ui_Dialog() 35 | self.ui.setupUi(self) 36 | self.setWindowTitle(u"%s - %s" % (self.tr("Layer Properties"), layer.name())) 37 | 38 | self.layer = layer 39 | self.initBlendingCombo() 40 | self.ui.horizontalSlider_Transparency.valueChanged.connect(self.sliderChanged) 41 | self.ui.spinBox_Transparency.valueChanged.connect(self.spinBoxChanged) 42 | self.ui.buttonBox.button(QDialogButtonBox.Apply).clicked.connect(self.applyClicked) 43 | 44 | self.ui.textEdit_Properties.setText(layer.metadata()) 45 | self.ui.spinBox_Transparency.setValue(layer.transparency) 46 | i = self.ui.comboBox_BlendingMode.findText(layer.blendModeName) 47 | if i != -1: 48 | self.ui.comboBox_BlendingMode.setCurrentIndex(i) 49 | 50 | if layer.layerDef.serviceUrl[0] == ":": 51 | self.ui.checkBox_SmoothRender.setEnabled(False) 52 | self.ui.checkBox_CreditVisibility.setEnabled(False) 53 | else: 54 | self.ui.checkBox_SmoothRender.setChecked(layer.smoothRender) 55 | self.ui.checkBox_CreditVisibility.setChecked(layer.creditVisibility) 56 | 57 | def initBlendingCombo(self): 58 | attrs = dir(QPainter) 59 | for attr in attrs: 60 | if attr.startswith("CompositionMode_"): 61 | self.ui.comboBox_BlendingMode.addItem(attr[16:]) 62 | 63 | def sliderChanged(self, val): 64 | s = self.ui.spinBox_Transparency 65 | s.blockSignals(True) 66 | s.setValue(val) 67 | s.blockSignals(False) 68 | 69 | def spinBoxChanged(self, val): 70 | s = self.ui.horizontalSlider_Transparency 71 | s.blockSignals(True) 72 | s.setValue(val) 73 | s.blockSignals(False) 74 | -------------------------------------------------------------------------------- /propertiesdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 438 10 | 367 11 | 12 | 13 | 14 | Properties 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Qt::Horizontal 23 | 24 | 25 | QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 0 35 | 36 | 37 | 38 | Style 39 | 40 | 41 | 42 | QLayout::SetDefaultConstraint 43 | 44 | 45 | 46 | 47 | 48 | 49 | Transparency 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 100 59 | 60 | 61 | Qt::Horizontal 62 | 63 | 64 | 65 | 66 | 67 | 68 | 100 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Blending mode 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | (Default: SourceOver) 90 | 91 | 92 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Smoothing 104 | 105 | 106 | 107 | 108 | 109 | 110 | Place the credit on the bottom right corner 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Properties 121 | 122 | 123 | 124 | 125 | 126 | true 127 | 128 | 129 | 80 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | buttonBox 144 | accepted() 145 | Dialog 146 | accept() 147 | 148 | 149 | 248 150 | 254 151 | 152 | 153 | 157 154 | 274 155 | 156 | 157 | 158 | 159 | buttonBox 160 | rejected() 161 | Dialog 162 | reject() 163 | 164 | 165 | 316 166 | 260 167 | 168 | 169 | 286 170 | 274 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /rotatedrect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | RotatedRect 5 | ------------------- 6 | begin : 2015-03-05 7 | copyright : (C) 2015 Minoru Akagi 8 | email : akaginch@gmail.com 9 | ***************************************************************************/ 10 | 11 | /*************************************************************************** 12 | * * 13 | * This program is free software; you can redistribute it and/or modify * 14 | * it under the terms of the GNU General Public License as published by * 15 | * the Free Software Foundation; either version 2 of the License, or * 16 | * (at your option) any later version. * 17 | * * 18 | ***************************************************************************/ 19 | """ 20 | import math 21 | from qgis.core import QGis, QgsPoint, QgsRectangle, QgsGeometry 22 | 23 | 24 | class RotatedRect: 25 | 26 | def __init__(self, center, width, height, rotation=0): 27 | """ 28 | args: 29 | center -- QgsPoint 30 | width, height -- float 31 | rotation -- int/float 32 | """ 33 | self._center = center 34 | self._width = width 35 | self._height = height 36 | self._rotation = rotation 37 | self._updateDerived() 38 | 39 | def clone(self): 40 | return RotatedRect(self._center, self._width, self._height, self._rotation) 41 | 42 | def _updateDerived(self): 43 | self._unrotated_rect = self._unrotatedRect() 44 | 45 | def _unrotatedRect(self): 46 | center = self._center 47 | half_width = self._width / 2 48 | half_height = self._height / 2 49 | return QgsRectangle(center.x() - half_width, center.y() - half_height, 50 | center.x() + half_width, center.y() + half_height) 51 | 52 | @staticmethod 53 | def rotatePoint(point, degrees, origin=None): 54 | """Rotate point around the origin""" 55 | theta = degrees * math.pi / 180 56 | c = math.cos(theta) 57 | s = math.sin(theta) 58 | x = point.x() 59 | y = point.y() 60 | 61 | if origin: 62 | x -= origin.x() 63 | y -= origin.y() 64 | 65 | # rotate counter-clockwise 66 | xd = x * c - y * s 67 | yd = x * s + y * c 68 | 69 | if origin: 70 | xd += origin.x() 71 | yd += origin.y() 72 | return QgsPoint(xd, yd) 73 | 74 | def normalizePoint(self, x, y): 75 | """Normalize given point. In result, lower-left is (0, 0) and upper-right is (1, 1).""" 76 | pt = QgsPoint(x, y) 77 | if self._rotation: 78 | pt = self.rotatePoint(pt, -self._rotation, self._center) 79 | rect = self._unrotated_rect 80 | return QgsPoint((pt.x() - rect.xMinimum()) / rect.width(), 81 | (pt.y() - rect.yMinimum()) / rect.height()) 82 | 83 | def scale(self, s): 84 | self._width *= s 85 | self._height *= s 86 | self._updateDerived() 87 | return self 88 | 89 | def rotate(self, degrees, origin=None): 90 | """Rotate the center of extent around the origin 91 | args: 92 | degrees -- int/float (counter-clockwise) 93 | origin -- QgsPoint 94 | """ 95 | self._rotation += degrees 96 | if origin is None: 97 | return self 98 | self._center = self.rotatePoint(self._center, degrees, origin) 99 | self._updateDerived() 100 | return self 101 | 102 | def point(self, norm_point, y_inverted=False): 103 | """ 104 | args: 105 | norm_point -- QgsPoint (0 <= x <= 1, 0 <= y <= 1) 106 | y_inverted -- If True, lower-left is (0, 1) and upper-right is (1, 0). 107 | Or else lower-left is (0, 0) and upper-right is (1, 1). 108 | """ 109 | ur_rect = self._unrotated_rect 110 | x = ur_rect.xMinimum() + norm_point.x() * ur_rect.width() 111 | if y_inverted: 112 | y = ur_rect.yMaximum() - norm_point.y() * ur_rect.height() 113 | else: 114 | y = ur_rect.yMinimum() + norm_point.y() * ur_rect.height() 115 | return self.rotatePoint(QgsPoint(x, y), self._rotation, self._center) 116 | 117 | def subrectangle(self, norm_rect, y_inverted=False): 118 | """ 119 | args: 120 | norm_rect -- QgsRectangle (0 <= xmin, 0 <= ymin, xmax <= 1, ymax <= 1) 121 | y_inverted -- If True, lower-left is (0, 1) and upper-right is (1, 0). 122 | Or else lower-left is (0, 0) and upper-right is (1, 1). 123 | """ 124 | ur_rect = self._unrotated_rect 125 | xmin = ur_rect.xMinimum() + norm_rect.xMinimum() * ur_rect.width() 126 | xmax = ur_rect.xMinimum() + norm_rect.xMaximum() * ur_rect.width() 127 | if y_inverted: 128 | ymin = ur_rect.yMaximum() - norm_rect.yMaximum() * ur_rect.height() 129 | ymax = ur_rect.yMaximum() - norm_rect.yMinimum() * ur_rect.height() 130 | else: 131 | ymin = ur_rect.yMinimum() + norm_rect.yMinimum() * ur_rect.height() 132 | ymax = ur_rect.yMinimum() + norm_rect.yMaximum() * ur_rect.height() 133 | 134 | rect = QgsRectangle(xmin, ymin, xmax, ymax) 135 | return RotatedRect(rect.center(), rect.width(), rect.height()).rotate(self._rotation, self._center) 136 | 137 | @classmethod 138 | def fromMapSettings(cls, mapSettings): 139 | extent = mapSettings.visibleExtent() if QGis.QGIS_VERSION_INT >= 20300 else mapSettings.extent() 140 | rotation = mapSettings.rotation() if QGis.QGIS_VERSION_INT >= 20700 else 0 141 | if rotation == 0: 142 | return cls(extent.center(), extent.width(), extent.height()) 143 | 144 | mupp = mapSettings.mapUnitsPerPixel() 145 | canvas_size = mapSettings.outputSize() 146 | return cls(extent.center(), mupp * canvas_size.width(), mupp * canvas_size.height(), rotation) 147 | 148 | def toMapSettings(self, mapSettings=None): 149 | if mapSettings is None: 150 | if QGis.QGIS_VERSION_INT >= 20300: 151 | from qgis.core import QgsMapSettings 152 | mapSettings = QgsMapSettings() 153 | else: 154 | return None 155 | mapSettings.setExtent(self._unrotated_rect) 156 | mapSettings.setRotation(self._rotation) 157 | return mapSettings 158 | 159 | def boundingBox(self): 160 | theta = self._rotation * math.pi / 180 161 | c = abs(math.cos(theta)) 162 | s = abs(math.sin(theta)) 163 | hw = (self._width * c + self._height * s) / 2 164 | hh = (self._width * s + self._height * c) / 2 165 | return QgsRectangle(self._center.x() - hw, self._center.y() - hh, 166 | self._center.x() + hw, self._center.y() + hh) 167 | 168 | def geotransform(self, cols, rows, is_grid_point=True): 169 | center = self._center 170 | ur_rect = self._unrotated_rect 171 | rotation = self._rotation 172 | 173 | segments_x = cols 174 | segments_y = rows 175 | if is_grid_point: 176 | segments_x -= 1 177 | segments_y -= 1 178 | 179 | if rotation: 180 | # rotate top-left corner of unrotated extent around center of extent counter-clockwise (map rotates clockwise) 181 | rpt = self.rotatePoint(QgsPoint(ur_rect.xMinimum(), ur_rect.yMaximum()), rotation, center) 182 | res_lr = self._width / segments_x 183 | res_ul = self._height / segments_y 184 | 185 | theta = rotation * math.pi / 180 186 | c = math.cos(theta) 187 | s = math.sin(theta) 188 | geotransform = [rpt.x(), res_lr * c, res_ul * s, rpt.y(), res_lr * s, -res_ul * c] 189 | if is_grid_point: 190 | # top-left corner of extent corresponds to center of top-left pixel. 191 | geotransform[0] -= 0.5 * geotransform[1] + 0.5 * geotransform[2] 192 | geotransform[3] -= 0.5 * geotransform[4] + 0.5 * geotransform[5] 193 | else: 194 | xres = self._width / segments_x 195 | yres = self._height / segments_y 196 | geotransform = [ur_rect.xMinimum(), xres, 0, ur_rect.yMaximum(), 0, -yres] 197 | if is_grid_point: 198 | geotransform[0] -= 0.5 * geotransform[1] 199 | geotransform[3] -= 0.5 * geotransform[5] 200 | 201 | return geotransform 202 | 203 | def center(self): 204 | return self._center 205 | 206 | def width(self): 207 | return self._width 208 | 209 | def height(self): 210 | return self._height 211 | 212 | def rotation(self): 213 | return self._rotation 214 | 215 | def unrotatedRect(self): 216 | return self._unrotated_rect 217 | 218 | def geometry(self): 219 | pts = self.vertices() 220 | pts.append(pts[0]) 221 | return QgsGeometry.fromPolygon([pts]) 222 | 223 | def vertices(self): 224 | """return vertices of the rect clockwise""" 225 | rect = self._unrotated_rect 226 | pts = [QgsPoint(rect.xMinimum(), rect.yMaximum()), 227 | QgsPoint(rect.xMaximum(), rect.yMaximum()), 228 | QgsPoint(rect.xMaximum(), rect.yMinimum()), 229 | QgsPoint(rect.xMinimum(), rect.yMinimum())] 230 | 231 | if self._rotation: 232 | return map(lambda pt: self.rotatePoint(pt, self._rotation, self._center), pts) 233 | 234 | return pts 235 | 236 | def __repr__(self): 237 | return "RotatedRect(c:{0}, w:{1}, h:{2}, r:{3})".format(self._center.toString(), self._width, self._height, self._rotation) 238 | 239 | # print coordinates of vertices 240 | pts = self.verticies() 241 | return "RotatedRect:" + ",".join(map(lambda (x, y): "P{0}({1})".format(x, y.toString()), enumerate(pts))) 242 | -------------------------------------------------------------------------------- /settingsdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | from PyQt4.QtCore import Qt, QSettings 23 | from PyQt4.QtGui import QDialog, QFileDialog 24 | 25 | from ui_settingsdialog import Ui_Dialog 26 | 27 | class SettingsDialog(QDialog): 28 | def __init__(self, iface): 29 | QDialog.__init__(self, iface.mainWindow()) 30 | # set up the user interface 31 | self.ui = Ui_Dialog() 32 | self.ui.setupUi(self) 33 | self.ui.toolButton_externalDirectory.clicked.connect(self.selectExternalDirectory) 34 | 35 | # load settings 36 | settings = QSettings() 37 | self.ui.lineEdit_externalDirectory.setText(settings.value("/TileLayerPlugin/extDir", "", type=unicode)) 38 | self.ui.spinBox_downloadTimeout.setValue(int(settings.value("/TileLayerPlugin/timeout", 30, type=int))) 39 | self.ui.checkBox_MoveToLayer.setCheckState(int(settings.value("/TileLayerPlugin/moveToLayer", 0, type=int))) 40 | self.ui.checkBox_NavigationMessages.setCheckState(int(settings.value("/TileLayerPlugin/naviMsg", Qt.Checked, type=int))) 41 | 42 | def accept(self): 43 | QDialog.accept(self) 44 | 45 | # save settings 46 | settings = QSettings() 47 | settings.setValue("/TileLayerPlugin/extDir", self.ui.lineEdit_externalDirectory.text()) 48 | settings.setValue("/TileLayerPlugin/timeout", self.ui.spinBox_downloadTimeout.value()) 49 | settings.setValue("/TileLayerPlugin/moveToLayer", self.ui.checkBox_MoveToLayer.checkState()) 50 | settings.setValue("/TileLayerPlugin/naviMsg", self.ui.checkBox_NavigationMessages.checkState()) 51 | 52 | def selectExternalDirectory(self): 53 | # show select directory dialog 54 | d = QFileDialog.getExistingDirectory(self, self.tr("Select external layers directory"), self.ui.lineEdit_externalDirectory.text()) 55 | if d: 56 | self.ui.lineEdit_externalDirectory.setText(d) 57 | -------------------------------------------------------------------------------- /settingsdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 512 10 | 143 11 | 12 | 13 | 14 | TileLayerPlugin Settings 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | External layer definition directory 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ... 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Download time-out (sec) 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 0 54 | 0 55 | 56 | 57 | 58 | 59 | 50 60 | 0 61 | 62 | 63 | 64 | 600 65 | 66 | 67 | 10 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Move plugin to Layer menu/toolbar 77 | 78 | 79 | 80 | 81 | 82 | 83 | Display navigation messages 84 | 85 | 86 | 87 | 88 | 89 | 90 | Qt::Horizontal 91 | 92 | 93 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | buttonBox 105 | accepted() 106 | Dialog 107 | accept() 108 | 109 | 110 | 248 111 | 254 112 | 113 | 114 | 157 115 | 274 116 | 117 | 118 | 119 | 120 | buttonBox 121 | rejected() 122 | Dialog 123 | reject() 124 | 125 | 126 | 316 127 | 260 128 | 129 | 130 | 286 131 | 274 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /tilelayer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | import math 23 | import threading 24 | 25 | from PyQt4.QtCore import Qt, Q_ARG, QEventLoop, QMetaObject, QObject, QPoint, QPointF, QRect, QRectF, QSettings, QUrl, QTimer, pyqtSignal, qDebug, QBuffer, QIODevice 26 | from PyQt4.QtGui import QBrush, QColor, QFont, QImage, QPainter, QMessageBox, QImageReader, QFileDialog 27 | from qgis.core import QGis, QgsApplication, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsGeometry, QgsPluginLayer, QgsPluginLayerType, QgsRectangle 28 | from qgis.gui import QgsMessageBar 29 | from os.path import join 30 | 31 | try: 32 | from osgeo import gdal 33 | hasGdal = True 34 | except: 35 | hasGdal = False 36 | 37 | from downloader import Downloader 38 | from rotatedrect import RotatedRect 39 | from tiles import BoundingBox, Tile, TileDefaultSettings, TileLayerDefinition, Tiles 40 | 41 | debug_mode = 1 42 | 43 | 44 | class TileLayer(QgsPluginLayer): 45 | 46 | LAYER_TYPE = "TileLayer" 47 | MAX_TILE_COUNT = 256 48 | DEFAULT_BLEND_MODE = "SourceOver" 49 | DEFAULT_SMOOTH_RENDER = True 50 | 51 | # PyQt signals 52 | statusSignal = pyqtSignal(str, int) 53 | messageBarSignal = pyqtSignal(str, str, int, int) 54 | 55 | def __init__(self, plugin, layerDef, creditVisibility=1): 56 | QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title) 57 | self.plugin = plugin 58 | self.iface = plugin.iface 59 | self.layerDef = layerDef 60 | self.creditVisibility = 1 if creditVisibility else 0 61 | self.tiles = None 62 | 63 | # set attribution property 64 | self.setAttribution(layerDef.attribution) 65 | 66 | # set custom properties 67 | self.setCustomProperty("title", layerDef.title) 68 | self.setCustomProperty("credit", layerDef.attribution) 69 | self.setCustomProperty("serviceUrl", layerDef.serviceUrl) 70 | self.setCustomProperty("yOriginTop", layerDef.yOriginTop) 71 | self.setCustomProperty("zmin", layerDef.zmin) 72 | self.setCustomProperty("zmax", layerDef.zmax) 73 | if layerDef.bbox: 74 | self.setCustomProperty("bbox", layerDef.bbox.toString()) 75 | self.setCustomProperty("creditVisibility", self.creditVisibility) 76 | 77 | # set crs 78 | if plugin.crs3857 is None: 79 | # create a QgsCoordinateReferenceSystem instance if plugin has no instance yet 80 | plugin.crs3857 = QgsCoordinateReferenceSystem(3857) 81 | 82 | self.setCrs(plugin.crs3857) 83 | 84 | # set extent 85 | if layerDef.bbox: 86 | self.setExtent(BoundingBox.degreesToMercatorMeters(layerDef.bbox).toQgsRectangle()) 87 | else: 88 | self.setExtent(QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) 89 | 90 | # set styles 91 | self.setTransparency(0) 92 | self.setBlendModeByName(self.DEFAULT_BLEND_MODE) 93 | self.setSmoothRender(self.DEFAULT_SMOOTH_RENDER) 94 | 95 | # downloader 96 | self.maxConnections = HonestAccess.maxConnections(layerDef.serviceUrl) 97 | self.cacheExpiry = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) 98 | self.userAgent = "QGIS/{0} TileLayerPlugin/{1}".format(QGis.QGIS_VERSION_INT, self.plugin.VERSION) # will be overwritten in QgsNetworkAccessManager::createRequest() since 2.2 99 | self.downloader = Downloader(self, self.maxConnections, self.cacheExpiry, self.userAgent) 100 | 101 | # TOS violation warning 102 | if HonestAccess.restrictedByTOS(layerDef.serviceUrl): 103 | QMessageBox.warning(None, 104 | u"{0} - {1}".format(self.plugin.pluginName, layerDef.title), 105 | self.tr("Access to the service is restricted by the TOS. Please follow the TOS.")) 106 | 107 | # multi-thread rendering 108 | if self.iface: 109 | self.statusSignal.connect(self.showStatusMessageSlot) 110 | self.messageBarSignal.connect(self.showMessageBarSlot) 111 | 112 | self.setValid(True) 113 | 114 | def setBlendModeByName(self, modeName): 115 | self.blendModeName = modeName 116 | blendMode = getattr(QPainter, "CompositionMode_" + modeName, 0) 117 | self.setBlendMode(blendMode) 118 | self.setCustomProperty("blendMode", modeName) 119 | 120 | def setTransparency(self, transparency): 121 | self.transparency = transparency 122 | self.setCustomProperty("transparency", transparency) 123 | 124 | def setSmoothRender(self, isSmooth): 125 | self.smoothRender = isSmooth 126 | self.setCustomProperty("smoothRender", 1 if isSmooth else 0) 127 | 128 | def setCreditVisibility(self, visible): 129 | self.creditVisibility = visible 130 | self.setCustomProperty("creditVisibility", 1 if visible else 0) 131 | 132 | def draw(self, renderContext): 133 | extent = renderContext.extent() 134 | if extent.isEmpty() or extent.width() == float("inf"): 135 | qDebug("Drawing is skipped because map extent is empty or inf.") 136 | return True 137 | 138 | map2pixel = renderContext.mapToPixel() 139 | mupp = map2pixel.mapUnitsPerPixel() 140 | rotation = map2pixel.mapRotation() 141 | 142 | painter = renderContext.painter() 143 | viewport = painter.viewport() 144 | 145 | isWebMercator = True 146 | transform = renderContext.coordinateTransform() 147 | if transform: 148 | isWebMercator = transform.destCRS().postgisSrid() == 3857 149 | 150 | # frame layer isn't drawn if the CRS is not web mercator or map is rotated 151 | if self.layerDef.serviceUrl[0] == ":" and "frame" in self.layerDef.serviceUrl: # or "number" in self.layerDef.serviceUrl: 152 | msg = "" 153 | if not isWebMercator: 154 | msg = self.tr("Frame layer is not drawn if the CRS is not EPSG:3857") 155 | elif rotation: 156 | msg = self.tr("Frame layer is not drawn if map is rotated") 157 | 158 | if msg: 159 | self.showMessageBar(msg, QgsMessageBar.INFO, 2) 160 | return True 161 | 162 | if not isWebMercator: 163 | # get extent in project CRS 164 | cx, cy = 0.5 * viewport.width(), 0.5 * viewport.height() 165 | center = map2pixel.toMapCoordinatesF(cx, cy) 166 | mapExtent = RotatedRect(center, mupp * viewport.width(), mupp * viewport.height(), rotation) 167 | 168 | if transform: 169 | transform = QgsCoordinateTransform(transform.destCRS(), transform.sourceCrs()) 170 | geometry = QgsGeometry.fromPolyline([map2pixel.toMapCoordinatesF(cx - 0.5, cy), map2pixel.toMapCoordinatesF(cx + 0.5, cy)]) 171 | geometry.transform(transform) 172 | mupp = geometry.length() 173 | 174 | # get bounding box of the extent in EPSG:3857 175 | geometry = mapExtent.geometry() 176 | geometry.transform(transform) 177 | extent = geometry.boundingBox() 178 | else: 179 | qDebug("Drawing is skipped because CRS transformation is not ready.") 180 | return True 181 | 182 | elif rotation: 183 | # get bounding box of the extent 184 | mapExtent = RotatedRect(extent.center(), mupp * viewport.width(), mupp * viewport.height(), rotation) 185 | extent = mapExtent.boundingBox() 186 | 187 | # calculate zoom level 188 | tile_mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE 189 | zoom = int(math.ceil(math.log(tile_mpp1 / mupp, 2) + 1)) 190 | zoom = max(0, min(zoom, self.layerDef.zmax)) 191 | #zoom = max(self.layerDef.zmin, zoom) 192 | 193 | # zoom limit 194 | if zoom < self.layerDef.zmin: 195 | if self.plugin.navigationMessagesEnabled: 196 | msg = self.tr("Current zoom level ({0}) is smaller than zmin ({1}): {2}").format(zoom, self.layerDef.zmin, self.layerDef.title) 197 | self.showMessageBar(msg, QgsMessageBar.INFO, 2) 198 | return True 199 | 200 | while True: 201 | # calculate tile range (yOrigin is top) 202 | size = self.layerDef.TSIZE1 / 2 ** (zoom - 1) 203 | matrixSize = 2 ** zoom 204 | ulx = max(0, int((extent.xMinimum() + self.layerDef.TSIZE1) / size)) 205 | uly = max(0, int((self.layerDef.TSIZE1 - extent.yMaximum()) / size)) 206 | lrx = min(int((extent.xMaximum() + self.layerDef.TSIZE1) / size), matrixSize - 1) 207 | lry = min(int((self.layerDef.TSIZE1 - extent.yMinimum()) / size), matrixSize - 1) 208 | 209 | # bounding box limit 210 | if self.layerDef.bbox: 211 | trange = self.layerDef.bboxDegreesToTileRange(zoom, self.layerDef.bbox) 212 | ulx = max(ulx, trange.xmin) 213 | uly = max(uly, trange.ymin) 214 | lrx = min(lrx, trange.xmax) 215 | lry = min(lry, trange.ymax) 216 | if lrx < ulx or lry < uly: 217 | # tile range is out of the bounding box 218 | return True 219 | 220 | # tile count limit 221 | tileCount = (lrx - ulx + 1) * (lry - uly + 1) 222 | if tileCount > self.MAX_TILE_COUNT: 223 | # as tile count is over the limit, decrease zoom level 224 | zoom -= 1 225 | 226 | # if the zoom level is less than the minimum, do not draw 227 | if zoom < self.layerDef.zmin: 228 | msg = self.tr("Tile count is over limit ({0}, max={1})").format(tileCount, self.MAX_TILE_COUNT) 229 | self.showMessageBar(msg, QgsMessageBar.WARNING, 4) 230 | return True 231 | continue 232 | 233 | # zoom level has been determined 234 | break 235 | 236 | self.logT("TileLayer.draw: {0} {1} {2} {3} {4}".format(zoom, ulx, uly, lrx, lry)) 237 | 238 | # save painter state 239 | painter.save() 240 | 241 | # set pen and font 242 | painter.setPen(Qt.black) 243 | font = QFont(painter.font()) 244 | font.setPointSize(10) 245 | painter.setFont(font) 246 | 247 | if self.layerDef.serviceUrl[0] == ":": 248 | painter.setBrush(QBrush(Qt.NoBrush)) 249 | self.drawDebugInfo(renderContext, zoom, ulx, uly, lrx, lry) 250 | else: 251 | # create a Tiles object and a list of urls to fetch tile image data 252 | tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) 253 | urls = [] 254 | cachedTiles = self.tiles 255 | cacheHits = 0 256 | for ty in range(uly, lry + 1): 257 | for tx in range(ulx, lrx + 1): 258 | data = None 259 | url = self.layerDef.tileUrl(zoom, tx, ty) 260 | if cachedTiles and zoom == cachedTiles.zoom and url in cachedTiles.tiles: 261 | data = cachedTiles.tiles[url].data 262 | tiles.addTile(url, Tile(zoom, tx, ty, data)) 263 | if data is None: 264 | urls.append(url) 265 | elif data: # memory cache exists 266 | cacheHits += 1 267 | # else: # tile not found 268 | 269 | self.tiles = tiles 270 | if len(urls) > 0: 271 | # fetch tile data 272 | files = self.fetchFiles(urls, renderContext) 273 | for url, data in files.items(): 274 | tiles.setImageData(url, data) 275 | 276 | if self.iface: 277 | stats = self.downloader.stats() 278 | allCacheHits = cacheHits + stats["cacheHits"] 279 | msg = self.tr("{0} files downloaded. {1} caches hit.").format(stats["downloaded"], allCacheHits) 280 | barmsg = None 281 | if self.downloader.errorStatus != Downloader.NO_ERROR: 282 | if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: 283 | barmsg = self.tr("Download Timeout - {0}").format(self.name()) 284 | elif stats["errors"] > 0: 285 | msg += self.tr(" {0} files failed.").format(stats["errors"]) 286 | if stats["successed"] + allCacheHits == 0: 287 | barmsg = self.tr("Failed to download all {0} files. - {1}").format(stats["errors"], self.name()) 288 | self.showStatusMessage(msg, 5000) 289 | if barmsg: 290 | self.showMessageBar(barmsg, QgsMessageBar.WARNING, 4) 291 | 292 | # apply layer style 293 | oldOpacity = painter.opacity() 294 | painter.setOpacity(0.01 * (100 - self.transparency)) 295 | oldSmoothRenderHint = painter.testRenderHint(QPainter.SmoothPixmapTransform) 296 | if self.smoothRender: 297 | painter.setRenderHint(QPainter.SmoothPixmapTransform) 298 | 299 | # do not start drawing tiles if rendering has been stopped 300 | if renderContext.renderingStopped(): 301 | self.log("draw(): renderingStopped!") 302 | painter.restore() 303 | return True 304 | 305 | # draw tiles 306 | if isWebMercator and rotation == 0: 307 | self.drawTiles(renderContext, tiles) 308 | # self.drawTilesDirectly(renderContext, tiles) 309 | else: 310 | # reproject tiles 311 | self.drawTilesOnTheFly(renderContext, mapExtent, tiles) 312 | 313 | # restore old state 314 | painter.setOpacity(oldOpacity) 315 | if self.smoothRender: 316 | painter.setRenderHint(QPainter.SmoothPixmapTransform, oldSmoothRenderHint) 317 | 318 | # draw credit on the bottom right corner 319 | if self.creditVisibility and self.layerDef.attribution: 320 | margin, paddingH, paddingV = (3, 4, 3) 321 | # scale 322 | scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) 323 | scale = max(scaleX, scaleY) 324 | painter.scale(scale, scale) 325 | 326 | visibleSWidth = painter.viewport().width() * scaleX / scale 327 | visibleSHeight = painter.viewport().height() * scaleY / scale 328 | rect = QRect(0, 0, visibleSWidth - margin, visibleSHeight - margin) 329 | textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.attribution) 330 | bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) 331 | painter.fillRect(bgRect, QColor(240, 240, 240, 150)) 332 | painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.attribution) 333 | 334 | # restore painter state 335 | painter.restore() 336 | return True 337 | 338 | def drawTiles(self, renderContext, tiles, sdx=1.0, sdy=1.0): 339 | # create an image that has the same resolution as the tiles 340 | image = tiles.image() 341 | 342 | # tile extent to pixel 343 | map2pixel = renderContext.mapToPixel() 344 | extent = tiles.extent() 345 | topLeft = map2pixel.transform(extent.xMinimum(), extent.yMaximum()) 346 | bottomRight = map2pixel.transform(extent.xMaximum(), extent.yMinimum()) 347 | rect = QRectF(QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) 348 | 349 | # draw the image on the map canvas 350 | renderContext.painter().drawImage(rect, image) 351 | 352 | self.log("drawTiles: {0} - {1}".format(str(extent), str(rect))) 353 | 354 | def drawTilesOnTheFly(self, renderContext, mapExtent, tiles, sdx=1.0, sdy=1.0): 355 | if not hasGdal: 356 | msg = self.tr("Rotation/Reprojection requires python-gdal") 357 | self.showMessageBar(msg, QgsMessageBar.INFO, 2) 358 | return 359 | 360 | transform = renderContext.coordinateTransform() 361 | if transform: 362 | sourceCrs = transform.sourceCrs() 363 | destCrs = transform.destCRS() 364 | else: 365 | sourceCrs = destCrs = self.crs() 366 | 367 | # create image from the tiles 368 | image = tiles.image() 369 | 370 | # tile extent 371 | extent = tiles.extent() 372 | geotransform = [extent.xMinimum(), extent.width() / image.width(), 0, extent.yMaximum(), 0, -extent.height() / image.height()] 373 | 374 | # source raster dataset 375 | driver = gdal.GetDriverByName("MEM") 376 | tile_ds = driver.Create("", image.width(), image.height(), 1, gdal.GDT_UInt32) 377 | tile_ds.SetProjection(str(sourceCrs.toWkt())) 378 | tile_ds.SetGeoTransform(geotransform) 379 | 380 | # QImage to raster 381 | ba = image.bits().asstring(image.numBytes()) 382 | tile_ds.GetRasterBand(1).WriteRaster(0, 0, image.width(), image.height(), ba) 383 | 384 | # target raster size - if smoothing is enabled, create raster of twice each of width and height of viewport size 385 | # in order to get high quality image 386 | oversampl = 2 if self.smoothRender else 1 387 | 388 | painter = renderContext.painter() 389 | viewport = painter.viewport() 390 | width, height = viewport.width() * oversampl, viewport.height() * oversampl 391 | 392 | # target raster dataset 393 | canvas_ds = driver.Create("", width, height, 1, gdal.GDT_UInt32) 394 | canvas_ds.SetProjection(str(destCrs.toWkt())) 395 | canvas_ds.SetGeoTransform(mapExtent.geotransform(width, height, is_grid_point=False)) 396 | 397 | # reproject image 398 | gdal.ReprojectImage(tile_ds, canvas_ds) 399 | 400 | # raster to QImage 401 | ba = canvas_ds.GetRasterBand(1).ReadRaster(0, 0, width, height) 402 | reprojected_image = QImage(ba, width, height, QImage.Format_ARGB32_Premultiplied) 403 | 404 | # draw the image on the map canvas 405 | rect = QRectF(QPointF(0, 0), QPointF(viewport.width() * sdx, viewport.height() * sdy)) 406 | painter.drawImage(rect, reprojected_image) 407 | 408 | def drawTilesDirectly(self, renderContext, tiles, sdx=1.0, sdy=1.0): 409 | p = renderContext.painter() 410 | for url, tile in tiles.tiles.items(): 411 | self.log("Draw tile: zoom: %d, x:%d, y:%d, data:%s" % (tile.zoom, tile.x, tile.y, str(tile.data))) 412 | rect = self.getTileRect(renderContext, tile.zoom, tile.x, tile.y, sdx, sdy) 413 | if tile.data: 414 | image = QImage() 415 | image.loadFromData(tile.data) 416 | p.drawImage(rect, image) 417 | 418 | def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry): 419 | painter = renderContext.painter() 420 | scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) 421 | painter.scale(scaleX, scaleY) 422 | 423 | if "frame" in self.layerDef.serviceUrl: 424 | self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) 425 | if "number" in self.layerDef.serviceUrl: 426 | self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) 427 | if "info" in self.layerDef.serviceUrl: 428 | self.drawInfo(renderContext, zoom, ulx, uly, lrx, lry) 429 | 430 | def drawFrame(self, renderContext, zoom, x, y, sdx, sdy): 431 | rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) 432 | p = renderContext.painter() 433 | #p.drawRect(rect) # A slash appears on the top-right tile without Antialiasing render hint. 434 | pts = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft(), rect.topLeft()] 435 | for i in range(4): 436 | p.drawLine(pts[i], pts[i + 1]) 437 | 438 | def drawFrames(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): 439 | for y in range(ymin, ymax + 1): 440 | for x in range(xmin, xmax + 1): 441 | self.drawFrame(renderContext, zoom, x, y, sdx, sdy) 442 | 443 | def drawNumber(self, renderContext, zoom, x, y, sdx, sdy): 444 | rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) 445 | p = renderContext.painter() 446 | if not self.layerDef.yOriginTop: 447 | y = (2 ** zoom - 1) - y 448 | p.drawText(rect, Qt.AlignCenter, "(%d, %d)\nzoom: %d" % (x, y, zoom)) 449 | 450 | def drawNumbers(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): 451 | for y in range(ymin, ymax + 1): 452 | for x in range(xmin, xmax + 1): 453 | self.drawNumber(renderContext, zoom, x, y, sdx, sdy) 454 | 455 | def drawInfo(self, renderContext, zoom, xmin, ymin, xmax, ymax): 456 | from debuginfo import drawDebugInformation 457 | drawDebugInformation(self, renderContext, zoom, xmin, ymin, xmax, ymax) 458 | 459 | def getScaleToVisibleExtent(self, renderContext): 460 | mapSettings = self.iface.mapCanvas().mapSettings() 461 | painter = renderContext.painter() 462 | if painter.device().logicalDpiX() == mapSettings.outputDpi(): 463 | return 1.0, 1.0 # scale should be 1.0 in rendering on map canvas 464 | 465 | extent = renderContext.extent() 466 | ct = renderContext.coordinateTransform() 467 | if ct: 468 | # FIX ME: want to get original visible extent in project CRS or visible view size in pixels 469 | 470 | # extent = ct.transformBoundingBox(extent) 471 | # xmax, ymin = extent.xMaximum(), extent.yMinimum() 472 | 473 | pt1 = ct.transform(extent.xMaximum(), extent.yMaximum()) 474 | pt2 = ct.transform(extent.xMaximum(), extent.yMinimum()) 475 | pt3 = ct.transform(extent.xMinimum(), extent.yMinimum()) 476 | xmax, ymin = min(pt1.x(), pt2.x()), max(pt2.y(), pt3.y()) 477 | else: 478 | xmax, ymin = extent.xMaximum(), extent.yMinimum() 479 | 480 | bottomRight = renderContext.mapToPixel().transform(xmax, ymin) 481 | viewport = painter.viewport() 482 | scaleX = bottomRight.x() / viewport.width() 483 | scaleY = bottomRight.y() / viewport.height() 484 | return scaleX, scaleY 485 | 486 | def getTileRect(self, renderContext, zoom, x, y, sdx=1.0, sdy=1.0, toInt=True): 487 | """ get tile pixel rect in the render context """ 488 | r = self.layerDef.getTileRect(zoom, x, y) 489 | map2pix = renderContext.mapToPixel() 490 | topLeft = map2pix.transform(r.xMinimum(), r.yMaximum()) 491 | bottomRight = map2pix.transform(r.xMaximum(), r.yMinimum()) 492 | if toInt: 493 | return QRect(QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) 494 | else: 495 | return QRectF(QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) 496 | 497 | def networkReplyFinished(self, url): 498 | # show progress 499 | stats = self.downloader.stats() 500 | msg = self.tr("{0} of {1} files downloaded.").format(stats["downloaded"], stats["total"]) 501 | errors = stats["errors"] 502 | if errors: 503 | msg += self.tr(" {0} files failed.").format(errors) 504 | self.showStatusMessage(msg) 505 | 506 | def readXml(self, node): 507 | self.readCustomProperties(node) 508 | self.layerDef.title = self.customProperty("title", "") 509 | self.layerDef.attribution = self.customProperty("credit", "") 510 | if self.layerDef.attribution == "": 511 | self.layerDef.attribution = self.customProperty("providerName", "") # for compatibility with 0.11 512 | self.layerDef.serviceUrl = self.customProperty("serviceUrl", "") 513 | self.layerDef.yOriginTop = int(self.customProperty("yOriginTop", 1)) 514 | self.layerDef.zmin = int(self.customProperty("zmin", TileDefaultSettings.ZMIN)) 515 | self.layerDef.zmax = int(self.customProperty("zmax", TileDefaultSettings.ZMAX)) 516 | bbox = self.customProperty("bbox", None) 517 | if bbox: 518 | self.layerDef.bbox = BoundingBox.fromString(bbox) 519 | self.setExtent(BoundingBox.degreesToMercatorMeters(self.layerDef.bbox).toQgsRectangle()) 520 | 521 | # layer style 522 | self.setTransparency(int(self.customProperty("transparency", 0))) 523 | self.setBlendModeByName(self.customProperty("blendMode", self.DEFAULT_BLEND_MODE)) 524 | self.setSmoothRender(int(self.customProperty("smoothRender", self.DEFAULT_SMOOTH_RENDER))) 525 | self.creditVisibility = int(self.customProperty("creditVisibility", 1)) 526 | 527 | # max connections of downloader 528 | self.maxConnections = HonestAccess.maxConnections(self.layerDef.serviceUrl) 529 | return True 530 | 531 | def writeXml(self, node, doc): 532 | element = node.toElement() 533 | element.setAttribute("type", "plugin") 534 | element.setAttribute("name", TileLayer.LAYER_TYPE) 535 | return True 536 | 537 | def readSymbology(self, node, errorMessage): 538 | return False 539 | 540 | def writeSymbology(self, node, doc, errorMessage): 541 | return False 542 | 543 | def metadata(self): 544 | lines = [] 545 | fmt = u"%s:\t%s" 546 | lines.append(fmt % (self.tr("Title"), self.layerDef.title)) 547 | lines.append(fmt % (self.tr("Attribution"), self.layerDef.attribution)) 548 | lines.append(fmt % (self.tr("URL"), self.layerDef.serviceUrl)) 549 | lines.append(fmt % (self.tr("yOrigin"), u"%s (yOriginTop=%d)" % (("Bottom", "Top")[self.layerDef.yOriginTop], self.layerDef.yOriginTop))) 550 | if self.layerDef.bbox: 551 | extent = self.layerDef.bbox.toString() 552 | else: 553 | extent = self.tr("Not set") 554 | lines.append(fmt % (self.tr("Zoom range"), "%d - %d" % (self.layerDef.zmin, self.layerDef.zmax))) 555 | lines.append(fmt % (self.tr("Layer Extent"), extent)) 556 | return "\n".join(lines) 557 | 558 | def fetchFiles(self, urls, renderContext): 559 | downloader = Downloader(None, self.maxConnections, self.cacheExpiry, self.userAgent) 560 | downloader.moveToThread(QgsApplication.instance().thread()) 561 | downloader.timer.moveToThread(QgsApplication.instance().thread()) 562 | 563 | self.logT("TileLayer.fetchFiles() starts") 564 | # create a QEventLoop object that belongs to the current worker thread 565 | eventLoop = QEventLoop() 566 | downloader.allRepliesFinished.connect(eventLoop.quit) 567 | if self.iface: 568 | # for download progress 569 | downloader.replyFinished.connect(self.networkReplyFinished) 570 | self.downloader = downloader 571 | 572 | # create a timer to watch whether rendering is stopped 573 | watchTimer = QTimer() 574 | watchTimer.timeout.connect(eventLoop.quit) 575 | 576 | # fetch files 577 | QMetaObject.invokeMethod(self.downloader, "fetchFilesAsync", Qt.QueuedConnection, Q_ARG(list, urls), Q_ARG(int, self.plugin.downloadTimeout)) 578 | 579 | # wait for the fetch to finish 580 | tick = 0 581 | interval = 500 582 | timeoutTick = self.plugin.downloadTimeout * 1000 / interval 583 | watchTimer.start(interval) 584 | while tick < timeoutTick: 585 | # run event loop for 0.5 seconds at maximum 586 | eventLoop.exec_() 587 | if downloader.unfinishedCount() == 0 or renderContext.renderingStopped(): 588 | break 589 | tick += 1 590 | watchTimer.stop() 591 | 592 | if downloader.unfinishedCount() > 0: 593 | downloader.abort(False) 594 | if tick == timeoutTick: 595 | downloader.errorStatus = Downloader.TIMEOUT_ERROR 596 | self.log("fetchFiles(): timeout") 597 | 598 | # watchTimer.timeout.disconnect(eventLoop.quit) 599 | # downloader.allRepliesFinished.disconnect(eventLoop.quit) 600 | 601 | self.logT("TileLayer.fetchFiles() ends") 602 | return downloader.fetchedFiles 603 | 604 | def showStatusMessage(self, msg, timeout=0): 605 | self.statusSignal.emit(msg, timeout) #TODO: use QMetaObject.invokeMethod 606 | 607 | def showStatusMessageSlot(self, msg, timeout): 608 | self.iface.mainWindow().statusBar().showMessage(msg, timeout) 609 | 610 | def showMessageBar(self, text, level=QgsMessageBar.INFO, duration=0, title=None): 611 | if title is None: 612 | title = self.plugin.pluginName 613 | self.messageBarSignal.emit(title, text, level, duration) 614 | 615 | def showMessageBarSlot(self, title, text, level, duration): 616 | self.iface.messageBar().pushMessage(title, text, level, duration) 617 | 618 | def log(self, msg): 619 | if debug_mode: 620 | qDebug(msg) 621 | 622 | def logT(self, msg): 623 | if debug_mode: 624 | qDebug("%s: %s" % (str(threading.current_thread()), msg)) 625 | 626 | def dump(self, detail=False, bbox=None): 627 | pass 628 | 629 | def saveTiles(self): 630 | if self.tiles is None: 631 | QMessageBox.warning(None, self.plugin.pluginName, self.tr("No tiles have been downloaded.")) 632 | return 633 | 634 | # Let the user choose the directory to save to 635 | directory = QFileDialog.getExistingDirectory(caption=self.tr("{}: Choose directory").format(self.layerDef.title)) 636 | if not directory: 637 | # User cancelled the directory selection 638 | return 639 | 640 | # Build the content of the .aux.xml file containing the projection info 641 | projection_string = (self.crs().toWkt()) 642 | pam_string = '\n' + \ 643 | '{}\n'.format(projection_string) + \ 644 | '' 645 | 646 | for tile in self.tiles.tiles.values(): 647 | # Figure out the file format extension 648 | reader = QImageReader() 649 | buffer = QBuffer() 650 | buffer.setData(tile.data) 651 | buffer.open(QIODevice.ReadOnly) 652 | extension = str(reader.imageFormat(buffer)) 653 | 654 | # Build the file name of the image file 655 | image_file_name = "{}-{}-{}.{}".format(tile.x, tile.y, tile.zoom, extension) 656 | image_file_path = join(directory, image_file_name) 657 | 658 | # Save the image file 659 | with open(image_file_path, 'wb') as image_file: 660 | image_file.write(tile.data) 661 | 662 | # Save the .aux.xml 663 | with open(image_file_path + '.aux.xml', 'w') as aux_file: 664 | aux_file.write(pam_string) 665 | 666 | # Save the world file containing the georeferencing information 667 | tile_rect = self.tiles.serviceInfo.getTileRect(tile.zoom, tile.x, tile.y) 668 | tile_size = self.tiles.TILE_SIZE 669 | with open(image_file_path + 'w', 'w') as world_file: 670 | world_file.writelines([ 671 | str(tile_rect.width() / tile_size) + '\n', 672 | '0\n', 673 | '0\n', 674 | str(-tile_rect.height() / tile_size) + '\n', 675 | str(tile_rect.xMinimum()) + '\n', 676 | str(tile_rect.yMaximum()) + '\n' 677 | ]) 678 | 679 | # Done 680 | msg = self.tr("Tiles have been saved.") 681 | self.showMessageBar(msg, QgsMessageBar.INFO, 2) 682 | 683 | # def createMapRenderer(self, renderContext): 684 | # return TileLayerRenderer(self, renderContext) 685 | 686 | 687 | # class TileLayerRenderer(QgsMapLayerRenderer): 688 | # 689 | # def __init__(self, layer, renderContext): 690 | # QgsMapLayerRenderer.__init__(self, layer.id()) 691 | # self.layer = layer 692 | # self.context = renderContext 693 | # 694 | # def render(self): 695 | # return self.layer.draw(self.context) 696 | 697 | 698 | class TileLayerType(QgsPluginLayerType): 699 | 700 | def __init__(self, plugin): 701 | QgsPluginLayerType.__init__(self, TileLayer.LAYER_TYPE) 702 | self.plugin = plugin 703 | 704 | def createLayer(self): 705 | layer = TileLayer(self.plugin, TileLayerDefinition.createEmptyInfo()) 706 | self.plugin.addActionToLayer(layer) 707 | return layer 708 | 709 | def showLayerProperties(self, layer): 710 | from propertiesdialog import PropertiesDialog 711 | dialog = PropertiesDialog(layer) 712 | dialog.applyClicked.connect(self.applyClicked) 713 | dialog.show() 714 | accepted = dialog.exec_() 715 | if accepted: 716 | self.applyProperties(dialog) 717 | return True 718 | 719 | def applyClicked(self): 720 | self.applyProperties(QObject().sender()) 721 | 722 | def applyProperties(self, dialog): 723 | layer = dialog.layer 724 | layer.setTransparency(dialog.ui.spinBox_Transparency.value()) 725 | layer.setBlendModeByName(dialog.ui.comboBox_BlendingMode.currentText()) 726 | layer.setSmoothRender(dialog.ui.checkBox_SmoothRender.isChecked()) 727 | layer.setCreditVisibility(dialog.ui.checkBox_CreditVisibility.isChecked()) 728 | layer.repaintRequested.emit() 729 | 730 | 731 | class HonestAccess: 732 | 733 | @staticmethod 734 | def maxConnections(url): 735 | host = QUrl(url).host() 736 | if "openstreetmap.org" in host: # http://wiki.openstreetmap.org/wiki/Tile_servers 737 | return 2 # http://wiki.openstreetmap.org/wiki/Tile_usage_policy 738 | return 6 739 | 740 | @staticmethod 741 | def restrictedByTOS(url): 742 | # whether access to the url is restricted by TOS 743 | host = QUrl(url).host() 744 | if "google.com" in host: # https://developers.google.com/maps/terms 10.1.1.a No Access to Maps API(s) Except... 745 | return True 746 | return False 747 | -------------------------------------------------------------------------------- /tilelayerplugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayerPlugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | import os 23 | 24 | from PyQt4.QtCore import Qt, QCoreApplication, QFile, QSettings, QTranslator, qVersion, qDebug 25 | from PyQt4.QtGui import QAction, QIcon 26 | from qgis.core import QGis, QgsCoordinateReferenceSystem, QgsMapLayerRegistry, QgsPluginLayerRegistry, QgsMapLayer 27 | 28 | from tilelayer import TileLayer, TileLayerType 29 | 30 | debug_mode = 1 31 | 32 | 33 | class TileLayerPlugin: 34 | 35 | VERSION = "0.80" 36 | 37 | def __init__(self, iface): 38 | # Save reference to the QGIS interface 39 | self.iface = iface 40 | # initialize plugin directory 41 | self.plugin_dir = os.path.dirname(QFile.decodeName(__file__)) 42 | # initialize locale 43 | settings = QSettings() 44 | locale = settings.value("locale/userLocale", "")[0:2] 45 | localePath = os.path.join(self.plugin_dir, 'i18n', '{0}.qm'.format(locale)) 46 | 47 | if os.path.exists(localePath): 48 | self.translator = QTranslator() 49 | self.translator.load(localePath) 50 | 51 | if qVersion() > '4.3.3': 52 | QCoreApplication.installTranslator(self.translator) 53 | 54 | self.pluginName = self.tr("TileLayerPlugin") 55 | self.downloadTimeout = int(settings.value("/TileLayerPlugin/timeout", 30, type=int)) 56 | self.navigationMessagesEnabled = int(settings.value("/TileLayerPlugin/naviMsg", Qt.Checked, type=int)) 57 | self.crs3857 = None 58 | self.layers = {} 59 | 60 | # register plugin layer type 61 | self.tileLayerType = TileLayerType(self) 62 | QgsPluginLayerRegistry.instance().addPluginLayerType(self.tileLayerType) 63 | 64 | # connect signal-slot 65 | QgsMapLayerRegistry.instance().layerRemoved.connect(self.layerRemoved) 66 | 67 | def initGui(self): 68 | # create action 69 | icon = QIcon(os.path.join(self.plugin_dir, "icon.png")) 70 | self.action = QAction(icon, self.tr("Add Tile Layer..."), self.iface.mainWindow()) 71 | self.action.setObjectName("TileLayerPlugin_AddLayer") 72 | 73 | # connect the action to the method 74 | self.action.triggered.connect(self.run) 75 | 76 | # add toolbar button and menu item 77 | if QSettings().value("/TileLayerPlugin/moveToLayer", 0, type=int): 78 | self.iface.insertAddLayerAction(self.action) 79 | self.iface.layerToolBar().addAction(self.action) 80 | else: 81 | self.iface.addPluginToWebMenu(self.pluginName, self.action) 82 | 83 | def unload(self): 84 | # remove the plugin menu item and icon 85 | if QSettings().value("/TileLayerPlugin/moveToLayer", 0, type=int): 86 | self.iface.layerToolBar().removeAction(self.action) 87 | self.iface.removeAddLayerAction(self.action) 88 | else: 89 | self.iface.removePluginWebMenu(self.pluginName, self.action) 90 | 91 | # unregister plugin layer type 92 | QgsPluginLayerRegistry.instance().removePluginLayerType(TileLayer.LAYER_TYPE) 93 | 94 | # disconnect signal-slot 95 | QgsMapLayerRegistry.instance().layerRemoved.disconnect(self.layerRemoved) 96 | 97 | def layerRemoved(self, layerId): 98 | if layerId in self.layers: 99 | self.iface.legendInterface().removeLegendLayerAction(self.layers[layerId].saveTilesAction) 100 | del self.layers[layerId] 101 | if debug_mode: 102 | qDebug("Layer %s removed" % layerId.encode("UTF-8")) 103 | 104 | def addTileLayer(self, layerdef, creditVisibility=True): 105 | """@api 106 | @param layerdef - an object of TileLayerDefinition class (in tiles.py) 107 | @param creditVisibility - visibility of credit label 108 | @returns newly created tile layer. if the layer is invalid, returns None 109 | @note added in 0.60 110 | """ 111 | if self.crs3857 is None: 112 | self.crs3857 = QgsCoordinateReferenceSystem(3857) 113 | 114 | layer = TileLayer(self, layerdef, creditVisibility) 115 | if not layer.isValid(): 116 | return None 117 | 118 | QgsMapLayerRegistry.instance().addMapLayer(layer) 119 | self.layers[layer.id()] = layer 120 | self.addActionToLayer(layer) 121 | return layer 122 | 123 | def addActionToLayer(self, layer): 124 | # Action for saving tiles 125 | layer.saveTilesAction = QAction(self.tr('Save tiles'), 126 | self.iface.legendInterface()) 127 | self.iface.legendInterface().addLegendLayerAction(layer.saveTilesAction, 128 | "", 129 | u"savetiles", 130 | QgsMapLayer.PluginLayer, 131 | False) 132 | 133 | self.iface.legendInterface().addLegendLayerActionForLayer(layer.saveTilesAction, layer) 134 | layer.saveTilesAction.triggered.connect(layer.saveTiles) 135 | 136 | def run(self): 137 | from addlayerdialog import AddLayerDialog 138 | dialog = AddLayerDialog(self) 139 | dialog.show() 140 | if dialog.exec_(): 141 | creditVisibility = dialog.ui.checkBox_CreditVisibility.isChecked() 142 | for layerdef in dialog.selectedLayerDefinitions(): 143 | self.addTileLayer(layerdef, creditVisibility) 144 | 145 | def settings(self): 146 | oldMoveToLayer = QSettings().value("/TileLayerPlugin/moveToLayer", 0, type=int) 147 | 148 | from settingsdialog import SettingsDialog 149 | dialog = SettingsDialog(self.iface) 150 | accepted = dialog.exec_() 151 | if not accepted: 152 | return False 153 | self.downloadTimeout = dialog.ui.spinBox_downloadTimeout.value() 154 | self.navigationMessagesEnabled = dialog.ui.checkBox_NavigationMessages.checkState() 155 | 156 | moveToLayer = dialog.ui.checkBox_MoveToLayer.checkState() 157 | if moveToLayer != oldMoveToLayer: 158 | if oldMoveToLayer: 159 | self.iface.layerToolBar().removeAction(self.action) 160 | self.iface.removeAddLayerAction(self.action) 161 | else: 162 | self.iface.removePluginWebMenu(self.pluginName, self.action) 163 | 164 | if moveToLayer: 165 | self.iface.insertAddLayerAction(self.action) 166 | self.iface.layerToolBar().addAction(self.action) 167 | else: 168 | self.iface.addPluginToWebMenu(self.pluginName, self.action) 169 | return True 170 | 171 | def tr(self, msg): 172 | return QCoreApplication.translate("TileLayerPlugin", msg) 173 | -------------------------------------------------------------------------------- /tiles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | TileLayer Plugin 5 | A QGIS plugin 6 | Plugin layer for Tile Maps 7 | ------------------- 8 | begin : 2012-12-16 9 | copyright : (C) 2013 by Minoru Akagi 10 | email : akaginch@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | import math 23 | 24 | from PyQt4.QtCore import QRect 25 | from PyQt4.QtGui import QImage, QPainter 26 | from qgis.core import QgsRectangle 27 | 28 | R = 6378137 29 | 30 | class TileDefaultSettings: 31 | 32 | ZMIN = 0 33 | ZMAX = 18 34 | 35 | def degreesToMercatorMeters(lon, lat): 36 | # formula: http://en.wikipedia.org/wiki/Mercator_projection#Mathematics_of_the_Mercator_projection 37 | x = R * lon * math.pi / 180 38 | y = R * math.log(math.tan((90 + lat) * math.pi / 360)) 39 | return x, y 40 | 41 | 42 | class BoundingBox: 43 | 44 | def __init__(self, xmin, ymin, xmax, ymax): 45 | self.xmin = xmin 46 | self.ymin = ymin 47 | self.xmax = xmax 48 | self.ymax = ymax 49 | 50 | def toQgsRectangle(self): 51 | return QgsRectangle(self.xmin, self.ymin, self.xmax, self.ymax) 52 | 53 | def toString(self, digitsAfterPoint=None): 54 | if digitsAfterPoint is None: 55 | return "%f,%f,%f,%f" % (self.xmin, self.ymin, self.xmax, self.ymax) 56 | return "%.{0}f,%.{0}f,%.{0}f,%.{0}f".format(digitsAfterPoint) % (self.xmin, self.ymin, self.xmax, self.ymax) 57 | 58 | @classmethod 59 | def degreesToMercatorMeters(cls, bbox): 60 | xmin, ymin = degreesToMercatorMeters(bbox.xmin, bbox.ymin) 61 | xmax, ymax = degreesToMercatorMeters(bbox.xmax, bbox.ymax) 62 | return BoundingBox(xmin, ymin, xmax, ymax) 63 | 64 | @classmethod 65 | def fromString(cls, s): 66 | a = map(float, s.split(",")) 67 | return BoundingBox(a[0], a[1], a[2], a[3]) 68 | 69 | 70 | class TileLayerDefinition: 71 | 72 | TILE_SIZE = 256 73 | TSIZE1 = 20037508.342789244 74 | 75 | def __init__(self, title, attribution, serviceUrl, yOriginTop=1, zmin=TileDefaultSettings.ZMIN, zmax=TileDefaultSettings.ZMAX, bbox=None): 76 | self.title = title 77 | self.attribution = attribution 78 | self.serviceUrl = serviceUrl 79 | self.yOriginTop = yOriginTop 80 | self.zmin = max(zmin, 0) 81 | self.zmax = zmax 82 | self.bbox = bbox 83 | 84 | def tileUrl(self, zoom, x, y): 85 | if not self.yOriginTop: 86 | y = (2 ** zoom - 1) - y 87 | return self.serviceUrl.replace("{z}", str(zoom)).replace("{x}", str(x)).replace("{y}", str(y)) 88 | 89 | def getTileRect(self, zoom, x, y): 90 | size = self.TSIZE1 / 2 ** (zoom - 1) 91 | return QgsRectangle(x * size - self.TSIZE1, self.TSIZE1 - y * size, (x + 1) * size - self.TSIZE1, self.TSIZE1 - (y + 1) * size) 92 | 93 | def degreesToTile(self, zoom, lon, lat): 94 | x, y = degreesToMercatorMeters(lon, lat) 95 | size = self.TSIZE1 / 2 ** (zoom - 1) 96 | tx = int((x + self.TSIZE1) / size) 97 | ty = int((self.TSIZE1 - y) / size) 98 | return tx, ty 99 | 100 | def bboxDegreesToTileRange(self, zoom, bbox): 101 | xmin, ymin = self.degreesToTile(zoom, bbox.xmin, bbox.ymax) 102 | xmax, ymax = self.degreesToTile(zoom, bbox.xmax, bbox.ymin) 103 | return BoundingBox(xmin, ymin, xmax, ymax) 104 | 105 | def __str__(self): 106 | return "%s (%s)" % (self.title, self.serviceUrl) 107 | 108 | def toArrayForTreeView(self): 109 | extent = "" 110 | if self.bbox: 111 | extent = self.bbox.toString(2) 112 | return [self.title, self.attribution, self.serviceUrl, "%d-%d" % (self.zmin, self.zmax), extent, self.yOriginTop] 113 | 114 | @classmethod 115 | def createEmptyInfo(cls): 116 | return TileLayerDefinition("", "", "") 117 | 118 | 119 | class Tile: 120 | def __init__(self, zoom, x, y, data=None): 121 | self.zoom = zoom 122 | self.x = x 123 | self.y = y 124 | self.data = data 125 | 126 | 127 | class Tiles: 128 | 129 | def __init__(self, zoom, xmin, ymin, xmax, ymax, serviceInfo): 130 | self.zoom = zoom 131 | self.xmin = xmin 132 | self.ymin = ymin 133 | self.xmax = xmax 134 | self.ymax = ymax 135 | self.TILE_SIZE = serviceInfo.TILE_SIZE 136 | self.TSIZE1 = serviceInfo.TSIZE1 137 | self.yOriginTop = serviceInfo.yOriginTop 138 | self.serviceInfo = serviceInfo 139 | self.tiles = {} 140 | 141 | def addTile(self, url, tile): 142 | self.tiles[url] = tile 143 | 144 | def setImageData(self, url, data): 145 | if url in self.tiles: 146 | self.tiles[url].data = data 147 | 148 | def image(self): 149 | width = (self.xmax - self.xmin + 1) * self.TILE_SIZE 150 | height = (self.ymax - self.ymin + 1) * self.TILE_SIZE 151 | image = QImage(width, height, QImage.Format_ARGB32_Premultiplied) 152 | p = QPainter(image) 153 | for tile in self.tiles.values(): 154 | if not tile.data: 155 | continue 156 | 157 | x = tile.x - self.xmin 158 | y = tile.y - self.ymin 159 | rect = QRect(x * self.TILE_SIZE, y * self.TILE_SIZE, self.TILE_SIZE, self.TILE_SIZE) 160 | 161 | timg = QImage() 162 | timg.loadFromData(tile.data) 163 | p.drawImage(rect, timg) 164 | return image 165 | 166 | def extent(self): 167 | size = self.TSIZE1 / 2 ** (self.zoom - 1) 168 | return QgsRectangle(self.xmin * size - self.TSIZE1, self.TSIZE1 - (self.ymax + 1) * size, 169 | (self.xmax + 1) * size - self.TSIZE1, self.TSIZE1 - self.ymin * size) 170 | -------------------------------------------------------------------------------- /ui_addlayerdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'D:\Users\minorua\.qgis2\python\developing_plugins\TileLayerPlugin\addlayerdialog.ui' 4 | # 5 | # Created: Fri Jun 26 10:14:35 2015 6 | # by: PyQt4 UI code generator 4.10.2 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | try: 13 | _fromUtf8 = QtCore.QString.fromUtf8 14 | except AttributeError: 15 | def _fromUtf8(s): 16 | return s 17 | 18 | try: 19 | _encoding = QtGui.QApplication.UnicodeUTF8 20 | def _translate(context, text, disambig): 21 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 22 | except AttributeError: 23 | def _translate(context, text, disambig): 24 | return QtGui.QApplication.translate(context, text, disambig) 25 | 26 | class Ui_Dialog(object): 27 | def setupUi(self, Dialog): 28 | Dialog.setObjectName(_fromUtf8("Dialog")) 29 | Dialog.resize(600, 400) 30 | self.gridLayout = QtGui.QGridLayout(Dialog) 31 | self.gridLayout.setObjectName(_fromUtf8("gridLayout")) 32 | self.verticalLayout = QtGui.QVBoxLayout() 33 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 34 | self.treeView = QtGui.QTreeView(Dialog) 35 | self.treeView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) 36 | self.treeView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) 37 | self.treeView.setObjectName(_fromUtf8("treeView")) 38 | self.verticalLayout.addWidget(self.treeView) 39 | self.checkBox_CreditVisibility = QtGui.QCheckBox(Dialog) 40 | self.checkBox_CreditVisibility.setEnabled(True) 41 | self.checkBox_CreditVisibility.setChecked(True) 42 | self.checkBox_CreditVisibility.setObjectName(_fromUtf8("checkBox_CreditVisibility")) 43 | self.verticalLayout.addWidget(self.checkBox_CreditVisibility) 44 | self.horizontalLayout_2 = QtGui.QHBoxLayout() 45 | self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) 46 | self.pushButton_Settings = QtGui.QPushButton(Dialog) 47 | self.pushButton_Settings.setObjectName(_fromUtf8("pushButton_Settings")) 48 | self.horizontalLayout_2.addWidget(self.pushButton_Settings) 49 | spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) 50 | self.horizontalLayout_2.addItem(spacerItem) 51 | self.pushButton_Add = QtGui.QPushButton(Dialog) 52 | self.pushButton_Add.setDefault(True) 53 | self.pushButton_Add.setObjectName(_fromUtf8("pushButton_Add")) 54 | self.horizontalLayout_2.addWidget(self.pushButton_Add) 55 | self.pushButton_Close = QtGui.QPushButton(Dialog) 56 | self.pushButton_Close.setObjectName(_fromUtf8("pushButton_Close")) 57 | self.horizontalLayout_2.addWidget(self.pushButton_Close) 58 | self.verticalLayout.addLayout(self.horizontalLayout_2) 59 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 60 | 61 | self.retranslateUi(Dialog) 62 | QtCore.QMetaObject.connectSlotsByName(Dialog) 63 | 64 | def retranslateUi(self, Dialog): 65 | Dialog.setWindowTitle(_translate("Dialog", "Add tile layer", None)) 66 | self.checkBox_CreditVisibility.setText(_translate("Dialog", "Place the credit on the bottom right corner", None)) 67 | self.pushButton_Settings.setText(_translate("Dialog", "Settings", None)) 68 | self.pushButton_Add.setText(_translate("Dialog", "Add", None)) 69 | self.pushButton_Close.setText(_translate("Dialog", "Close", None)) 70 | 71 | -------------------------------------------------------------------------------- /ui_propertiesdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'D:\Users\minorua\.qgis2\python\developing_plugins\TileLayerPlugin\propertiesdialog.ui' 4 | # 5 | # Created: Fri Jun 26 10:14:42 2015 6 | # by: PyQt4 UI code generator 4.10.2 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | try: 13 | _fromUtf8 = QtCore.QString.fromUtf8 14 | except AttributeError: 15 | def _fromUtf8(s): 16 | return s 17 | 18 | try: 19 | _encoding = QtGui.QApplication.UnicodeUTF8 20 | def _translate(context, text, disambig): 21 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 22 | except AttributeError: 23 | def _translate(context, text, disambig): 24 | return QtGui.QApplication.translate(context, text, disambig) 25 | 26 | class Ui_Dialog(object): 27 | def setupUi(self, Dialog): 28 | Dialog.setObjectName(_fromUtf8("Dialog")) 29 | Dialog.resize(438, 367) 30 | self.gridLayout = QtGui.QGridLayout(Dialog) 31 | self.gridLayout.setObjectName(_fromUtf8("gridLayout")) 32 | self.gridLayout_2 = QtGui.QGridLayout() 33 | self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) 34 | self.buttonBox = QtGui.QDialogButtonBox(Dialog) 35 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal) 36 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Apply|QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) 37 | self.buttonBox.setObjectName(_fromUtf8("buttonBox")) 38 | self.gridLayout_2.addWidget(self.buttonBox, 3, 0, 1, 1) 39 | self.groupBox_Style = QtGui.QGroupBox(Dialog) 40 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) 41 | sizePolicy.setHorizontalStretch(0) 42 | sizePolicy.setVerticalStretch(0) 43 | sizePolicy.setHeightForWidth(self.groupBox_Style.sizePolicy().hasHeightForWidth()) 44 | self.groupBox_Style.setSizePolicy(sizePolicy) 45 | self.groupBox_Style.setObjectName(_fromUtf8("groupBox_Style")) 46 | self.verticalLayout = QtGui.QVBoxLayout(self.groupBox_Style) 47 | self.verticalLayout.setSizeConstraint(QtGui.QLayout.SetDefaultConstraint) 48 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 49 | self.formLayout = QtGui.QFormLayout() 50 | self.formLayout.setObjectName(_fromUtf8("formLayout")) 51 | self.label = QtGui.QLabel(self.groupBox_Style) 52 | self.label.setObjectName(_fromUtf8("label")) 53 | self.formLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.label) 54 | self.horizontalLayout_3 = QtGui.QHBoxLayout() 55 | self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) 56 | self.horizontalSlider_Transparency = QtGui.QSlider(self.groupBox_Style) 57 | self.horizontalSlider_Transparency.setMaximum(100) 58 | self.horizontalSlider_Transparency.setOrientation(QtCore.Qt.Horizontal) 59 | self.horizontalSlider_Transparency.setObjectName(_fromUtf8("horizontalSlider_Transparency")) 60 | self.horizontalLayout_3.addWidget(self.horizontalSlider_Transparency) 61 | self.spinBox_Transparency = QtGui.QSpinBox(self.groupBox_Style) 62 | self.spinBox_Transparency.setMaximum(100) 63 | self.spinBox_Transparency.setObjectName(_fromUtf8("spinBox_Transparency")) 64 | self.horizontalLayout_3.addWidget(self.spinBox_Transparency) 65 | self.formLayout.setLayout(1, QtGui.QFormLayout.FieldRole, self.horizontalLayout_3) 66 | self.label_2 = QtGui.QLabel(self.groupBox_Style) 67 | self.label_2.setObjectName(_fromUtf8("label_2")) 68 | self.formLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.label_2) 69 | self.horizontalLayout = QtGui.QHBoxLayout() 70 | self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) 71 | self.comboBox_BlendingMode = QtGui.QComboBox(self.groupBox_Style) 72 | self.comboBox_BlendingMode.setObjectName(_fromUtf8("comboBox_BlendingMode")) 73 | self.horizontalLayout.addWidget(self.comboBox_BlendingMode) 74 | self.label_3 = QtGui.QLabel(self.groupBox_Style) 75 | self.label_3.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 76 | self.label_3.setObjectName(_fromUtf8("label_3")) 77 | self.horizontalLayout.addWidget(self.label_3) 78 | self.formLayout.setLayout(2, QtGui.QFormLayout.FieldRole, self.horizontalLayout) 79 | self.verticalLayout.addLayout(self.formLayout) 80 | self.checkBox_SmoothRender = QtGui.QCheckBox(self.groupBox_Style) 81 | self.checkBox_SmoothRender.setObjectName(_fromUtf8("checkBox_SmoothRender")) 82 | self.verticalLayout.addWidget(self.checkBox_SmoothRender) 83 | self.checkBox_CreditVisibility = QtGui.QCheckBox(self.groupBox_Style) 84 | self.checkBox_CreditVisibility.setObjectName(_fromUtf8("checkBox_CreditVisibility")) 85 | self.verticalLayout.addWidget(self.checkBox_CreditVisibility) 86 | self.gridLayout_2.addWidget(self.groupBox_Style, 2, 0, 1, 1) 87 | self.groupBox_Properties = QtGui.QGroupBox(Dialog) 88 | self.groupBox_Properties.setObjectName(_fromUtf8("groupBox_Properties")) 89 | self.gridLayout_3 = QtGui.QGridLayout(self.groupBox_Properties) 90 | self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) 91 | self.textEdit_Properties = QtGui.QTextEdit(self.groupBox_Properties) 92 | self.textEdit_Properties.setReadOnly(True) 93 | self.textEdit_Properties.setTabStopWidth(80) 94 | self.textEdit_Properties.setObjectName(_fromUtf8("textEdit_Properties")) 95 | self.gridLayout_3.addWidget(self.textEdit_Properties, 0, 0, 1, 1) 96 | self.gridLayout_2.addWidget(self.groupBox_Properties, 0, 0, 1, 1) 97 | self.gridLayout.addLayout(self.gridLayout_2, 0, 0, 1, 1) 98 | 99 | self.retranslateUi(Dialog) 100 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), Dialog.accept) 101 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), Dialog.reject) 102 | QtCore.QMetaObject.connectSlotsByName(Dialog) 103 | 104 | def retranslateUi(self, Dialog): 105 | Dialog.setWindowTitle(_translate("Dialog", "Properties", None)) 106 | self.groupBox_Style.setTitle(_translate("Dialog", "Style", None)) 107 | self.label.setText(_translate("Dialog", "Transparency", None)) 108 | self.label_2.setText(_translate("Dialog", "Blending mode", None)) 109 | self.label_3.setText(_translate("Dialog", "(Default: SourceOver)", None)) 110 | self.checkBox_SmoothRender.setText(_translate("Dialog", "Smoothing", None)) 111 | self.checkBox_CreditVisibility.setText(_translate("Dialog", "Place the credit on the bottom right corner", None)) 112 | self.groupBox_Properties.setTitle(_translate("Dialog", "Properties", None)) 113 | 114 | -------------------------------------------------------------------------------- /ui_settingsdialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'D:\Users\minorua\.qgis2\python\developing_plugins\TileLayerPlugin\settingsdialog.ui' 4 | # 5 | # Created: Sat Aug 16 14:13:51 2014 6 | # by: PyQt4 UI code generator 4.10.2 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | try: 13 | _fromUtf8 = QtCore.QString.fromUtf8 14 | except AttributeError: 15 | def _fromUtf8(s): 16 | return s 17 | 18 | try: 19 | _encoding = QtGui.QApplication.UnicodeUTF8 20 | def _translate(context, text, disambig): 21 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 22 | except AttributeError: 23 | def _translate(context, text, disambig): 24 | return QtGui.QApplication.translate(context, text, disambig) 25 | 26 | class Ui_Dialog(object): 27 | def setupUi(self, Dialog): 28 | Dialog.setObjectName(_fromUtf8("Dialog")) 29 | Dialog.resize(512, 143) 30 | self.gridLayout = QtGui.QGridLayout(Dialog) 31 | self.gridLayout.setObjectName(_fromUtf8("gridLayout")) 32 | self.verticalLayout = QtGui.QVBoxLayout() 33 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 34 | self.formLayout = QtGui.QFormLayout() 35 | self.formLayout.setObjectName(_fromUtf8("formLayout")) 36 | self.label = QtGui.QLabel(Dialog) 37 | self.label.setObjectName(_fromUtf8("label")) 38 | self.formLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.label) 39 | self.horizontalLayout = QtGui.QHBoxLayout() 40 | self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) 41 | self.lineEdit_externalDirectory = QtGui.QLineEdit(Dialog) 42 | self.lineEdit_externalDirectory.setObjectName(_fromUtf8("lineEdit_externalDirectory")) 43 | self.horizontalLayout.addWidget(self.lineEdit_externalDirectory) 44 | self.toolButton_externalDirectory = QtGui.QToolButton(Dialog) 45 | self.toolButton_externalDirectory.setObjectName(_fromUtf8("toolButton_externalDirectory")) 46 | self.horizontalLayout.addWidget(self.toolButton_externalDirectory) 47 | self.formLayout.setLayout(0, QtGui.QFormLayout.FieldRole, self.horizontalLayout) 48 | self.label_2 = QtGui.QLabel(Dialog) 49 | self.label_2.setObjectName(_fromUtf8("label_2")) 50 | self.formLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.label_2) 51 | self.spinBox_downloadTimeout = QtGui.QSpinBox(Dialog) 52 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) 53 | sizePolicy.setHorizontalStretch(0) 54 | sizePolicy.setVerticalStretch(0) 55 | sizePolicy.setHeightForWidth(self.spinBox_downloadTimeout.sizePolicy().hasHeightForWidth()) 56 | self.spinBox_downloadTimeout.setSizePolicy(sizePolicy) 57 | self.spinBox_downloadTimeout.setMinimumSize(QtCore.QSize(50, 0)) 58 | self.spinBox_downloadTimeout.setMaximum(600) 59 | self.spinBox_downloadTimeout.setSingleStep(10) 60 | self.spinBox_downloadTimeout.setObjectName(_fromUtf8("spinBox_downloadTimeout")) 61 | self.formLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.spinBox_downloadTimeout) 62 | self.verticalLayout.addLayout(self.formLayout) 63 | self.checkBox_MoveToLayer = QtGui.QCheckBox(Dialog) 64 | self.checkBox_MoveToLayer.setObjectName(_fromUtf8("checkBox_MoveToLayer")) 65 | self.verticalLayout.addWidget(self.checkBox_MoveToLayer) 66 | self.checkBox_NavigationMessages = QtGui.QCheckBox(Dialog) 67 | self.checkBox_NavigationMessages.setObjectName(_fromUtf8("checkBox_NavigationMessages")) 68 | self.verticalLayout.addWidget(self.checkBox_NavigationMessages) 69 | self.buttonBox = QtGui.QDialogButtonBox(Dialog) 70 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal) 71 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) 72 | self.buttonBox.setObjectName(_fromUtf8("buttonBox")) 73 | self.verticalLayout.addWidget(self.buttonBox) 74 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 75 | 76 | self.retranslateUi(Dialog) 77 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), Dialog.accept) 78 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), Dialog.reject) 79 | QtCore.QMetaObject.connectSlotsByName(Dialog) 80 | 81 | def retranslateUi(self, Dialog): 82 | Dialog.setWindowTitle(_translate("Dialog", "TileLayerPlugin Settings", None)) 83 | self.label.setText(_translate("Dialog", "External layer definition directory", None)) 84 | self.toolButton_externalDirectory.setText(_translate("Dialog", "...", None)) 85 | self.label_2.setText(_translate("Dialog", "Download time-out (sec)", None)) 86 | self.checkBox_MoveToLayer.setText(_translate("Dialog", "Move plugin to Layer menu/toolbar", None)) 87 | self.checkBox_NavigationMessages.setText(_translate("Dialog", "Display navigation messages", None)) 88 | 89 | --------------------------------------------------------------------------------