├── .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 |
--------------------------------------------------------------------------------