├── .gitignore ├── AUTHORS.txt ├── CHANGELOG.txt ├── LICENSE.txt ├── Makefile ├── README.txt ├── __init__.py ├── imageexport.py ├── imageexport.ui ├── imageexport_ui.py ├── mapnikglobe.png ├── mapnikhelp.png ├── metadata.txt ├── print2pixel.py ├── quantumnik.py ├── relativism.py ├── release ├── remake ├── render_wrapper.py ├── resources.qrc ├── sync.py ├── text_editor.py ├── text_editor.ui └── text_editor_ui.py /.gitignore: -------------------------------------------------------------------------------- 1 | resources.py 2 | *.pyc 3 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | 4 | Dane Springmeyer 5 | Aaron Racicot 6 | 7 | With help from: 8 | 9 | Alex Mandel 10 | Bob Moskovitz 11 | Tim Sinnott 12 | Tim Sutton 13 | Ahmed Dassouki 14 | Tyler Mitchell 15 | Will White 16 | Robert Soden 17 | Jürgen Fischer 18 | Josh Livni 19 | Michal Migurksi 20 | Artem Pavlenko 21 | Gary Sherman 22 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Quantumnik Changelog 2 | -------------------- 3 | 4 | Version 0.4.1, June 29, 2011: 5 | ----------------------------- 6 | 7 | * Improved handling of user/custom projections 8 | 9 | 10 | Version 0.4.0, June 29, 2011: 11 | ----------------------------- 12 | 13 | * Upgrade to QGIS 1.7.0 and latest Mapnik2 (svn r2996) 14 | 15 | 16 | Version 0.3.9, Feb 18, 2011: 17 | ---------------------------- 18 | 19 | * Fix display on windows broken in 0.3.8 20 | 21 | 22 | Version 0.3.8, Nov 27, 2010: 23 | ---------------------------- 24 | 25 | * Added support for Cascadenik 1.x 26 | 27 | * Avoided redundant creation of datasources, speeding up conversion from QGIS 28 | canvas to a mapnik.Map 29 | 30 | * Fixed bug with reading shapefiles in a directory by dispatching to ogr 31 | 32 | * Speedup rendering by writing mapnik.Image string directly to QImage 33 | 34 | 35 | Version 0.3.7, May 1, 2010: 36 | --------------------------- 37 | 38 | * Support for GPX layers through OGR provider 39 | 40 | 41 | Version 0.3.6, April 15th, 2010: 42 | -------------------------------- 43 | 44 | * Maintain support for Mapnik trunk, now using `mapnik2` namespace 45 | * Upgrading Qt signals - http://trac.osgeo.org/qgis/ticket/1743 46 | * Dropped use of line numbers when using pygments syntax highlighting 47 | 48 | 49 | Version 0.3.5, March 9th, 2010: 50 | ------------------------------- 51 | 52 | * Better support for attribute-scaled point symbols (requires Mapnik >=0.8.0) 53 | * Added support for using images as polygon fill (QGIS custom texture --> Mapnik 54 | PolygonPatternSymbolizer) 55 | * Fixed support for hollow fill point symbols 56 | 57 | 58 | Version 0.3.4, February 3rd, 2010: 59 | ---------------------------------- 60 | 61 | * Better support for the QGIS OpenStreetMap Plugin by rendering from sqlite db 62 | 63 | 64 | Version 0.3.3, February 2nd, 2010: 65 | ---------------------------------- 66 | 67 | * Added beta support for rendering of .osm files loaded in QGIS 68 | * Added support for Mapnik 0.8.0-dev (working trunk) 69 | * Improved support for loading Cascadenik files 70 | * Fixed regression in support for graduated symbols 71 | * Added support for pointz and multipoint shapefiles through Mapnik OGR plugin 72 | 73 | 74 | Version 0.3.2, January 12th, 2010: 75 | ---------------------------------- 76 | 77 | * Fixed regression in labeling from 0.3.1 78 | 79 | 80 | Version 0.3.1, January 12th, 2010: 81 | ---------------------------------- 82 | 83 | * Better support for loading existing XML/MML and live editing. 84 | * Added support for better writing of cairo formats and polygon gamma correction 85 | available in upcoming Mapnik 0.7.0 release. 86 | * Added support for Mapnik2 (trunk). 87 | * Improved default display of dashed outlines on polygons. 88 | * Improved default of text label vertical alignment (to support Mapnik 0.7.0). 89 | 90 | 91 | Version 0.3.0, December 17th, 2009: 92 | ----------------------------------- 93 | 94 | * Implemented new aggregate styles approach as progress toward zoom-dependent styles. 95 | - http://bitbucket.org/springmeyer/quantumnik/issue/8/ 96 | - new minor version as this change greatly re-worked the approach for understanding 97 | and creating Mapnik layers from QGIS layers. 98 | 99 | * Fixed a few bugs with rasters and projections from 0.2.7 and 0.2.8 100 | 101 | 102 | Version 0.2.8, December 12th, 2009: 103 | ----------------------------------- 104 | 105 | * Repaired support for using custom postgres schemas. 106 | 107 | 108 | Version 0.2.7, December 11th, 2009: 109 | ----------------------------------- 110 | 111 | * Ensured that relative paths to point symbols and file datasources are used 112 | when exporting to XML. 113 | * Made the layer name and projection xml output more concise. 114 | 115 | 116 | Version 0.2.6, December 9th, 2009: 117 | ---------------------------------- 118 | 119 | * Fixed bug with handling of duplicate styles/layers 120 | * Proper assignment of 'min_distance' and 'spacing' parameters for TextSymbolizer 121 | - Should result in few duplicate road names on linear features 122 | * Allow for live viewing of XML that was loaded from Mapnik xml or Cascadenik mml 123 | * Better error messages for windows users 124 | 125 | 126 | Version 0.2.5, November 30th, 2009: 127 | ----------------------------------- 128 | 129 | * For now, skip Symbology-NG layers that QGIS trunk (soon to be 1.4) supports 130 | 131 | 132 | Version 0.2.4, November 30th, 2009: 133 | ----------------------------------- 134 | 135 | * Turned off layer queryability for faster rendering 136 | * Added handling of text halo color (QGIS calls label "buffer") 137 | * Fixed rare case of potential zero division error in continuous color renderer 138 | * Added ability to read in multipoint shapefiles using OGR plugin 139 | * Fixed bug in imageexport. 140 | 141 | 142 | Version 0.2.3, November 13th, 2009: 143 | ----------------------------------- 144 | 145 | * Restore compatibility with QGIS 1.0.x 146 | - Keyboard shortcuts not possible before 1.2 147 | 148 | 149 | Version 0.2.2, November 10th, 2009: 150 | ----------------------------------- 151 | 152 | * Various bugfixes 153 | 154 | 155 | Version 0.2.1, November 10th, 2009: 156 | ----------------------------------- 157 | 158 | * Added tabbed layout for QGIS/Mapnik maps (racicot) 159 | * Added keyboard shortcuts to switch between tabs 160 | - Ctrl-[ to switch to QGIS 161 | - Ctrl-] to switch to Mapnik 162 | * Added support for reloading XML or MML rendered in QGIS 163 | - Clicking main icon will reload 164 | - Ctrl-R (Apple-R on osx) will also reload 165 | - This makes live editing XML in a separate editor easier 166 | * Added syntax highlighting to XML viewer via pygements (if installed) 167 | 168 | 169 | Version 0.2.0, October 7st, 2009: 170 | --------------------------------- 171 | 172 | * Tagged from r93 173 | * Improved support for PostGIS schemas 174 | * Improved support for continuous color renderer when using PostGIS float8 data types 175 | * Restored support for QGIS 1.x series (broken in 0.1.9) 176 | 177 | 178 | Version 0.1.9, October 1st, 2009: 179 | --------------------------------- 180 | 181 | * Tagged from r85 182 | * Fixed scaling of image symbols 183 | 184 | 185 | Version 0.1.8, September 28, 2009: 186 | ---------------------------------- 187 | 188 | * Tagged from r80 189 | * Repaired broken support for on-the-fly reprojection 190 | * Remove projection warning when only one layer is being used 191 | and the qgis map<->layer srs differs without using on-the-fly reprojection 192 | 193 | 194 | Version 0.1.7, August 17 2009: 195 | ------------------------------ 196 | 197 | * Tagged from r76 198 | * Added more friendly message for unsupported formats. 199 | * Added draft (but currently disable) support for WMS layers. 200 | * Added support for Grass Vectors through OGR. 201 | * Added support for the QGIS SpatialLite Provider. 202 | 203 | Version 0.1.6, August 13, 2009: 204 | ------------------------------- 205 | 206 | * Tagged from r66 207 | * Special thanks to cgs_bob for excellent testing and reports. 208 | * Added better cross-platform support for file paths (Issue #16) 209 | * Added preliminary support for handling e00/arc adf files through OGR driver. 210 | * Added workaround for when the QGIS map srs does not match the layer srs (http://trac.osgeo.org/qgis/ticket/1688) by assuming equality. 211 | * Added ability to view live Mapnik XML in a dockable window (The plan longer term is for this to be editable). 212 | * Reworked and improved the 'Image Export' functionality, including rendering to a temporary file, writing to Cairo supported PS, PDF, and SVG formats, and controlling the background transparency (Printing option still not implemented) 213 | * Improved creation of Mapnik filters based on better handling of QGIS field types (Issue #13) 214 | * Improved handling of OGR datasources 215 | 216 | 217 | Version 0.1.5, June 7, 2009: 218 | ---------------------------- 219 | * Tagged from r44 220 | * Better QVariant handling 221 | * Added label based scale-visibility 222 | * Improved support for continuous color gradients 223 | * Added support for unique values symbolization on float values 224 | 225 | 226 | Version 0.1.4, June 4, 2009: 227 | ---------------------------- 228 | * Packaged from r41 229 | * Minor Bugfixes 230 | 231 | 232 | Version 0.1.3, June 4, 2009: 233 | ---------------------------- 234 | * Packaged from r40 235 | * Add proper support for OGR datasources (r32) 236 | * Add support for Mapnik 0.5.x for vector layers (r31) 237 | * Add rudimentary support for continuous color gradients (r31) 238 | * Add support for PostGIS custom ports/host/pass/schema/sql (r28,r29,r30) 239 | * Add support for handling active layers individually, to control rendering by Qgis and Mapnik independently (r26,r36,r37) 240 | 241 | 242 | Version 0.1.2, May 25, 2009: 243 | ---------------------------- 244 | * Use y-axis text displacement by default for labels along lines from migurski (r25) 245 | * Add support for dash-arrays (r24) 246 | * Add support for subtle line cap/join/pen/brush translation (r23) 247 | 248 | 249 | Version 0.1.1, May 15, 2009: 250 | ---------------------------- 251 | * Added rudimentary support for point symbolization using PNGs exported from QGIS (r20) 252 | * Added support for Rasters and min/max visibility for layers (r19) 253 | * Added warning when projections are not properly set in QGIS (r18) 254 | * Added support for PointSymbolizer displacement 255 | * Better handling of QString unicode -> string conversion for Filters 256 | 257 | 258 | Version 0.1.0, May 1, 2009: 259 | --------------------------- 260 | 261 | * First release of plugin 262 | * Two classes compose main plugin and Qgis->Mapnik interface 263 | * Support for reading of XML/MML and writing of XML 264 | * Support for Shapefiles and PostGIS 265 | * Support for dynamic rendering on QGIS MapCanvas 266 | * Support for unique values and single symbolization -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010, Dane Springmeyer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INST_DIR = ~/.qgis/python/plugins/quantumnik_dev 2 | 3 | #PYRCC = /System/Library/Frameworks/Python.framework/Versions/2.5/bin/pyrcc4 4 | #PYUIC = /System/Library/Frameworks/Python.framework/Versions/2.5/bin/pyuic4 5 | 6 | PYRCC = pyrcc4 7 | PYUIC = pyuic4 8 | 9 | RC_PY_FILE = resources.py 10 | 11 | all: quantumnik 12 | 13 | quantumnik: $(RC_PY_FILE) imageexport_ui.py text_editor_ui.py 14 | 15 | install: quantumnik 16 | mkdir -p $(INST_DIR) 17 | cp *.py $(INST_DIR)/ 18 | cp *.png $(INST_DIR)/ 19 | chmod -R 777 $(INST_DIR)/ 20 | 21 | clean: 22 | rm -f $(RC_PY_FILE) 23 | rm -f *.pyc 24 | 25 | $(RC_PY_FILE): resources.qrc 26 | $(PYRCC) -o $(RC_PY_FILE) resources.qrc 27 | 28 | imageexport_ui.py: imageexport.ui 29 | $(PYUIC) -o imageexport_ui.py imageexport.ui 30 | 31 | text_editor_ui.py: text_editor.ui 32 | $(PYUIC) -o text_editor_ui.py text_editor.ui 33 | 34 | .PHONY: install clean -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Quantumnik 2 | ---------- 3 | 4 | Plugin download is available within QGIS and at: 5 | 6 | http://plugins.qgis.org/plugins/quantumnik/ 7 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from quantumnik import Quantumnik, NAME 4 | 5 | def name(): 6 | return NAME 7 | 8 | def description(): 9 | return "Mapnik integration with QGIS" 10 | 11 | def version(): 12 | return "Version 0.4.1" 13 | 14 | def qgisMinimumVersion(): 15 | return "1.8" # Kore, ideally 1.1.0 (Pan) 16 | 17 | def author(): 18 | return "Dane Springmeyer" 19 | 20 | def email(): 21 | return "dane@dbsgeo.com" 22 | 23 | def homepage(): 24 | return "https://github.com/springmeyer/quantumnik" 25 | 26 | def classFactory(iface): 27 | return Quantumnik(iface) 28 | -------------------------------------------------------------------------------- /imageexport.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import sync 6 | import time 7 | import tempfile 8 | import render_wrapper 9 | from PyQt4.QtCore import * 10 | from PyQt4.QtGui import * 11 | from imageexport_ui import Ui_ImageExport 12 | 13 | try: 14 | import mapnik2 as mapnik 15 | except ImportError: 16 | import mapnik 17 | 18 | class ImageExport(QDialog, Ui_ImageExport): 19 | def __init__(self, parent, flags=None): 20 | QDialog.__init__(self, parent.iface.mainWindow(), flags) 21 | # Set up the user interface from Designer. 22 | self.parent = parent 23 | self.setupUi(self) 24 | QObject.connect(self.image_output_button, SIGNAL("clicked()"), self.setSaveFile) 25 | QObject.connect(self.tmp_render, SIGNAL("clicked(bool)"), self.toggle_tmp_render) 26 | QObject.connect(self.auto_open, SIGNAL("clicked(bool)"), self.toggle_auto_open) 27 | QObject.connect(self.transparent_background, SIGNAL("clicked(bool)"), self.toggle_transparent_background) 28 | #self.width.setText("600") 29 | #self.height.setText("400") 30 | self.format.setCurrentIndex(self.format.findText("png")) 31 | self.use_auto_open = False 32 | self.use_tmp_file = False 33 | self.use_transparent_background = False 34 | 35 | #QObject.connect(self.format, SIGNAL("textChanged(QString)"), self.switch_format) 36 | #QObject.connect(self.format, SIGNAL("textIndexChanged(QString)"), self.switch_format) 37 | #QObject.connect(self.format, SIGNAL("textChanged(QString)"), self.switch_format) 38 | QObject.connect(self.format, SIGNAL("highlighted(QString)"), self.switch_format) 39 | 40 | # setup drop 41 | self.image_output_path.__class__.dragEnterEvent = self.path_drag 42 | self.image_output_path.__class__.dropEvent = self.path_drop 43 | 44 | if self.parent.last_image_path: 45 | self.image_output_path.setText(self.parent.last_image_path) 46 | else: 47 | from qgis.core import QgsProject as project 48 | project_name = os.path.basename(str(project.instance().fileName())) 49 | if project_name: 50 | project_name = project_name.split('.')[0] 51 | else: 52 | project_name = 'mapnik_map' 53 | # todo - use temp directory! 54 | self.image_output_path.setText("~/quantumnik/%s.%s" % (project_name,self.format.currentText())) 55 | 56 | #add the custom paper sizes to the combobox 57 | self.add_sizes() 58 | 59 | if self.parent.from_mapfile: 60 | self.resolution.setEnabled(False) 61 | self.resolution.setText("Not supported") 62 | else: 63 | self.resolution.setText("90") 64 | # set up QSettings 65 | self.settings = QSettings("dbsgeo","Quantumnik") 66 | 67 | def switch_format(self,new_format): 68 | path = self.image_output_path.text() 69 | parts = path.split('.') 70 | if len(parts) > 1: 71 | new_text = path.replace('.%s' % parts[1],'.%s' % new_format) 72 | self.image_output_path.setText(new_text) 73 | 74 | def toggle_transparent_background(self, isChecked): 75 | self.use_transparent_background = isChecked 76 | 77 | def toggle_auto_open(self, isChecked): 78 | self.use_auto_open = isChecked 79 | 80 | def toggle_tmp_render(self, isChecked): 81 | self.image_output_path.setEnabled(not isChecked) 82 | self.image_output_button.setEnabled(not isChecked) 83 | self.use_tmp_file = isChecked 84 | 85 | def add_sizes(self): 86 | from print2pixel import north_america as na,iso,jis,ansi 87 | names = [] 88 | for item in na.items(): 89 | name = QString("NA: %s %s (in)" % item) 90 | self.sizes.addItem(name) 91 | for item in ansi.items(): 92 | name = QString("ANSI: %s %s (in)" % item) 93 | self.sizes.addItem(name) 94 | for item in iso.items(): 95 | name = QString("ISO: %s %s (mm)" % item) 96 | self.sizes.addItem(name) 97 | for item in jis.items(): 98 | name = QString("Japanese: %s %s (mm)" % item) 99 | self.sizes.addItem(name) 100 | #self.sizes.setItemText(0, QApplication.translate("ImageExport", "test", None, QApplication.UnicodeUTF8)) 101 | 102 | def path_drag(self, event): 103 | if event.mimeData().hasUrls(): 104 | event.acceptProposedAction() 105 | 106 | def path_drop(self, event): 107 | urls = event.mimeData().urls(); 108 | file = str(urls[0].path()) 109 | self.image_output_path.setText(file) 110 | self.parent.last_image_path = file 111 | event.acceptProposedAction() 112 | #event.ignore() 113 | 114 | def dimensions(self): 115 | #if self.use_map_dimensions: 116 | return self.parent.canvas.width(),self.parent.canvas.height() 117 | #else: 118 | # pass 119 | #paper = 120 | #size = 121 | #w,h = 122 | 123 | def setSaveFile(self): 124 | map_path = str(self.settings.value("path/last_map_path",QVariant(".")).toString()) 125 | mapFile = QFileDialog.getSaveFileName(self, "Name for the file", \ 126 | map_path, "Image (*.png *.jpeg *.pdf *.svg *.ps)","Filter list for selecting files from a dialog box") 127 | self.image_output_path.setText(mapFile) 128 | self.parent.last_image_path = mapFile 129 | if mapFile: 130 | self.settings.setValue("path/last_map_path",QVariant(os.path.dirname(str(mapFile)))) 131 | 132 | def view_file(self,file_name,app=None): 133 | import platform 134 | if '~' in file_name: 135 | file_name = os.path.expanduser(file_name) 136 | try: 137 | if os.name == 'nt': 138 | if app: 139 | QMessageBox.information(self.parent.iface.mainWindow(),"Information", 'Overriding default image viewer not yet supported on Win32') 140 | os.system('start %s' % file_name.replace('/','\\')) 141 | #os.system('start "%s"' % os.path.dirname(file_name)) 142 | elif platform.uname()[0] == 'Linux': 143 | if app: 144 | os.system('%s "%s"' % (app, file_name)) 145 | else: 146 | # todo - this is unlikely to work... 147 | os.system('gthumb %s' % file_name) 148 | elif platform.uname()[0] == 'Darwin': 149 | if app: 150 | os.system('open %s -a "%s"' % (file_name, app)) 151 | else: 152 | os.system('open "%s"' % file_name) 153 | except Exception, e: 154 | QMessageBox.information(self.parent.iface.mainWindow(),"Information", 'Problem auto-opening image: %s' % e) 155 | 156 | # http://diotavelli.net/PyQtWiki/Threading,_Signals_and_Slots 157 | def render_thread(self,m,out,format): 158 | worky,result = render_wrapper.render_to_file(m,out,format) 159 | if worky: 160 | if self.use_auto_open: 161 | self.view_file(out) 162 | else: 163 | QMessageBox.information(self.parent.iface.mainWindow(),"Information", "Rendered to %s" % out) 164 | else: 165 | QMessageBox.information(self.parent.iface.mainWindow(),"Error", 'Sorry, export failed likely because your Mapnik install does not support the %s format. Error was: %s' % (format,result)) 166 | 167 | def accept(self): 168 | w,h = self.dimensions() 169 | if self.parent.from_mapfile: 170 | m = self.parent.mapnik_map 171 | else: 172 | m = sync.EasyCanvas(self.parent,self.parent.canvas,int(self.resolution.text())).to_mapnik() 173 | e = self.parent.canvas.extent() 174 | bbox = mapnik.Envelope(e.xMinimum(),e.yMinimum(),e.xMaximum(),e.yMaximum()) 175 | m.zoom_to_box(bbox) 176 | 177 | format = str(self.format.currentText()) 178 | 179 | if self.use_transparent_background: 180 | m.background = mapnik.Color('transparent') 181 | elif not self.parent.from_mapfile: 182 | m.background = self.parent.background 183 | 184 | if self.use_tmp_file: 185 | (handle, out) = tempfile.mkstemp('.%s' % format, 'quantumnik_output') 186 | os.close(handle) 187 | else: 188 | out = str(self.image_output_path.text()) 189 | 190 | self.render_thread(m,out,format) 191 | 192 | #import thread 193 | #thread.start_new_thread(self.render_thread, (m,out,format)) 194 | #return 195 | -------------------------------------------------------------------------------- /imageexport.ui: -------------------------------------------------------------------------------- 1 | 2 | Dane Springmeyer 3 | ImageExport 4 | 5 | 6 | Qt::NonModal 7 | 8 | 9 | true 10 | 11 | 12 | 13 | 0 14 | 0 15 | 356 16 | 386 17 | 18 | 19 | 20 | Qt::NoContextMenu 21 | 22 | 23 | Quantumnik - Image Export 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 5 32 | 345 33 | 334 34 | 32 35 | 36 | 37 | 38 | 39 | 0 40 | 0 41 | 42 | 43 | 44 | 45 | 11 46 | 47 | 48 | 49 | QDialogButtonBox::Close|QDialogButtonBox::Save 50 | 51 | 52 | 53 | 54 | 55 | 19 56 | 6 57 | 313 58 | 154 59 | 60 | 61 | 62 | 63 | 0 64 | 0 65 | 66 | 67 | 68 | 69 | 12 70 | 71 | 72 | 73 | Image Output 74 | 75 | 76 | 77 | 78 | 79 | 80 | 10 81 | 82 | 83 | 84 | Name for the map file to be created from the QGIS project file 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 10 93 | 50 94 | false 95 | PreferAntialias 96 | 97 | 98 | 99 | Qt::TabFocus 100 | 101 | 102 | Save As... 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 11 113 | 114 | 115 | 116 | Resolution: 117 | 118 | 119 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 120 | 121 | 122 | format 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 0 131 | 0 132 | 133 | 134 | 135 | 136 | 50 137 | 16777215 138 | 139 | 140 | 141 | 142 | 11 143 | 144 | 145 | 146 | false 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 11 158 | 159 | 160 | 161 | (ppi) 162 | 163 | 164 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 165 | 166 | 167 | format 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 11 176 | 177 | 178 | 179 | Format: 180 | 181 | 182 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 183 | 184 | 185 | format 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 11 194 | 195 | 196 | 197 | 198 | png 199 | 200 | 201 | 202 | 203 | jpeg 204 | 205 | 206 | 207 | 208 | pdf 209 | 210 | 211 | 212 | 213 | svg 214 | 215 | 216 | 217 | 218 | ps 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 11 230 | 231 | 232 | 233 | Render to tmp file... 234 | 235 | 236 | 237 | 238 | 239 | 240 | true 241 | 242 | 243 | 244 | 11 245 | 246 | 247 | 248 | Open after save... 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 11 257 | 258 | 259 | 260 | Transparent Background 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 20 270 | 175 271 | 316 272 | 156 273 | 274 | 275 | 276 | 277 | 0 278 | 0 279 | 280 | 281 | 282 | 283 | 12 284 | 285 | 286 | 287 | Print Options (Not Yet Enabled) 288 | 289 | 290 | 291 | 292 | 293 | false 294 | 295 | 296 | 297 | 11 298 | 299 | 300 | 301 | Custom Size: 302 | 303 | 304 | 305 | 306 | 307 | 308 | true 309 | 310 | 311 | 312 | 10 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | false 321 | 322 | 323 | 324 | 11 325 | 326 | 327 | 328 | Draw Neatline 329 | 330 | 331 | 332 | 333 | 334 | 335 | false 336 | 337 | 338 | 339 | 10 340 | 341 | 342 | 343 | 344 | Landscape 345 | 346 | 347 | 348 | 349 | Portrait 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | false 358 | 359 | 360 | 361 | 11 362 | 363 | 364 | 365 | Text Override: 366 | 367 | 368 | 369 | 370 | 371 | 372 | false 373 | 374 | 375 | 376 | 11 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | qPixmapFromMimeSource 386 | 387 | 388 | 389 | buttonBox 390 | accepted() 391 | ImageExport 392 | accept() 393 | 394 | 395 | 335 396 | 422 397 | 398 | 399 | 240 400 | 223 401 | 402 | 403 | 404 | 405 | buttonBox 406 | rejected() 407 | ImageExport 408 | reject() 409 | 410 | 411 | 426 412 | 422 413 | 414 | 415 | 240 416 | 223 417 | 418 | 419 | 420 | 421 | 422 | -------------------------------------------------------------------------------- /imageexport_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'imageexport.ui' 4 | # 5 | # Created: Thu Oct 1 14:53:37 2009 6 | # by: PyQt4 UI code generator 4.4.4 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | class Ui_ImageExport(object): 13 | def setupUi(self, ImageExport): 14 | ImageExport.setObjectName("ImageExport") 15 | ImageExport.setWindowModality(QtCore.Qt.NonModal) 16 | ImageExport.setEnabled(True) 17 | ImageExport.resize(356, 386) 18 | ImageExport.setContextMenuPolicy(QtCore.Qt.NoContextMenu) 19 | ImageExport.setSizeGripEnabled(True) 20 | self.buttonBox = QtGui.QDialogButtonBox(ImageExport) 21 | self.buttonBox.setGeometry(QtCore.QRect(5, 345, 334, 32)) 22 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) 23 | sizePolicy.setHorizontalStretch(0) 24 | sizePolicy.setVerticalStretch(0) 25 | sizePolicy.setHeightForWidth(self.buttonBox.sizePolicy().hasHeightForWidth()) 26 | self.buttonBox.setSizePolicy(sizePolicy) 27 | font = QtGui.QFont() 28 | font.setPointSize(11) 29 | self.buttonBox.setFont(font) 30 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Close|QtGui.QDialogButtonBox.Save) 31 | self.buttonBox.setObjectName("buttonBox") 32 | self.groupBox = QtGui.QGroupBox(ImageExport) 33 | self.groupBox.setGeometry(QtCore.QRect(19, 6, 313, 154)) 34 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) 35 | sizePolicy.setHorizontalStretch(0) 36 | sizePolicy.setVerticalStretch(0) 37 | sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) 38 | self.groupBox.setSizePolicy(sizePolicy) 39 | font = QtGui.QFont() 40 | font.setPointSize(12) 41 | self.groupBox.setFont(font) 42 | self.groupBox.setObjectName("groupBox") 43 | self.gridLayout = QtGui.QGridLayout(self.groupBox) 44 | self.gridLayout.setObjectName("gridLayout") 45 | self.image_output_path = QtGui.QLineEdit(self.groupBox) 46 | font = QtGui.QFont() 47 | font.setPointSize(10) 48 | self.image_output_path.setFont(font) 49 | self.image_output_path.setObjectName("image_output_path") 50 | self.gridLayout.addWidget(self.image_output_path, 0, 0, 1, 2) 51 | self.image_output_button = QtGui.QPushButton(self.groupBox) 52 | font = QtGui.QFont() 53 | font.setPointSize(10) 54 | font.setWeight(50) 55 | font.setBold(False) 56 | self.image_output_button.setFont(font) 57 | self.image_output_button.setFocusPolicy(QtCore.Qt.TabFocus) 58 | self.image_output_button.setObjectName("image_output_button") 59 | self.gridLayout.addWidget(self.image_output_button, 0, 2, 1, 1) 60 | self.horizontalLayout = QtGui.QHBoxLayout() 61 | self.horizontalLayout.setObjectName("horizontalLayout") 62 | self.textLabel4_2 = QtGui.QLabel(self.groupBox) 63 | font = QtGui.QFont() 64 | font.setPointSize(11) 65 | self.textLabel4_2.setFont(font) 66 | self.textLabel4_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 67 | self.textLabel4_2.setObjectName("textLabel4_2") 68 | self.horizontalLayout.addWidget(self.textLabel4_2) 69 | self.resolution = QtGui.QLineEdit(self.groupBox) 70 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) 71 | sizePolicy.setHorizontalStretch(0) 72 | sizePolicy.setVerticalStretch(0) 73 | sizePolicy.setHeightForWidth(self.resolution.sizePolicy().hasHeightForWidth()) 74 | self.resolution.setSizePolicy(sizePolicy) 75 | self.resolution.setMaximumSize(QtCore.QSize(50, 16777215)) 76 | font = QtGui.QFont() 77 | font.setPointSize(11) 78 | self.resolution.setFont(font) 79 | self.resolution.setAutoFillBackground(False) 80 | self.resolution.setObjectName("resolution") 81 | self.horizontalLayout.addWidget(self.resolution) 82 | self.textLabel4_3 = QtGui.QLabel(self.groupBox) 83 | font = QtGui.QFont() 84 | font.setPointSize(11) 85 | self.textLabel4_3.setFont(font) 86 | self.textLabel4_3.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 87 | self.textLabel4_3.setObjectName("textLabel4_3") 88 | self.horizontalLayout.addWidget(self.textLabel4_3) 89 | self.textLabel4 = QtGui.QLabel(self.groupBox) 90 | font = QtGui.QFont() 91 | font.setPointSize(11) 92 | self.textLabel4.setFont(font) 93 | self.textLabel4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 94 | self.textLabel4.setObjectName("textLabel4") 95 | self.horizontalLayout.addWidget(self.textLabel4) 96 | self.format = QtGui.QComboBox(self.groupBox) 97 | font = QtGui.QFont() 98 | font.setPointSize(11) 99 | self.format.setFont(font) 100 | self.format.setObjectName("format") 101 | self.format.addItem(QtCore.QString()) 102 | self.format.addItem(QtCore.QString()) 103 | self.format.addItem(QtCore.QString()) 104 | self.format.addItem(QtCore.QString()) 105 | self.format.addItem(QtCore.QString()) 106 | self.horizontalLayout.addWidget(self.format) 107 | self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 3) 108 | self.tmp_render = QtGui.QCheckBox(self.groupBox) 109 | font = QtGui.QFont() 110 | font.setPointSize(11) 111 | self.tmp_render.setFont(font) 112 | self.tmp_render.setObjectName("tmp_render") 113 | self.gridLayout.addWidget(self.tmp_render, 2, 0, 1, 1) 114 | self.auto_open = QtGui.QCheckBox(self.groupBox) 115 | self.auto_open.setEnabled(True) 116 | font = QtGui.QFont() 117 | font.setPointSize(11) 118 | self.auto_open.setFont(font) 119 | self.auto_open.setObjectName("auto_open") 120 | self.gridLayout.addWidget(self.auto_open, 2, 1, 1, 2) 121 | self.transparent_background = QtGui.QCheckBox(self.groupBox) 122 | font = QtGui.QFont() 123 | font.setPointSize(11) 124 | self.transparent_background.setFont(font) 125 | self.transparent_background.setObjectName("transparent_background") 126 | self.gridLayout.addWidget(self.transparent_background, 3, 0, 1, 3) 127 | self.grpMap = QtGui.QGroupBox(ImageExport) 128 | self.grpMap.setGeometry(QtCore.QRect(20, 175, 316, 156)) 129 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) 130 | sizePolicy.setHorizontalStretch(0) 131 | sizePolicy.setVerticalStretch(0) 132 | sizePolicy.setHeightForWidth(self.grpMap.sizePolicy().hasHeightForWidth()) 133 | self.grpMap.setSizePolicy(sizePolicy) 134 | font = QtGui.QFont() 135 | font.setPointSize(12) 136 | self.grpMap.setFont(font) 137 | self.grpMap.setObjectName("grpMap") 138 | self.gridLayout_2 = QtGui.QGridLayout(self.grpMap) 139 | self.gridLayout_2.setObjectName("gridLayout_2") 140 | self.label = QtGui.QLabel(self.grpMap) 141 | self.label.setEnabled(False) 142 | font = QtGui.QFont() 143 | font.setPointSize(11) 144 | self.label.setFont(font) 145 | self.label.setObjectName("label") 146 | self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) 147 | self.sizes = QtGui.QComboBox(self.grpMap) 148 | self.sizes.setEnabled(True) 149 | font = QtGui.QFont() 150 | font.setPointSize(10) 151 | self.sizes.setFont(font) 152 | self.sizes.setObjectName("sizes") 153 | self.gridLayout_2.addWidget(self.sizes, 0, 1, 1, 2) 154 | self.neatline = QtGui.QCheckBox(self.grpMap) 155 | self.neatline.setEnabled(False) 156 | font = QtGui.QFont() 157 | font.setPointSize(11) 158 | self.neatline.setFont(font) 159 | self.neatline.setObjectName("neatline") 160 | self.gridLayout_2.addWidget(self.neatline, 1, 0, 1, 2) 161 | self.comboBox = QtGui.QComboBox(self.grpMap) 162 | self.comboBox.setEnabled(False) 163 | font = QtGui.QFont() 164 | font.setPointSize(10) 165 | self.comboBox.setFont(font) 166 | self.comboBox.setObjectName("comboBox") 167 | self.comboBox.addItem(QtCore.QString()) 168 | self.comboBox.addItem(QtCore.QString()) 169 | self.gridLayout_2.addWidget(self.comboBox, 1, 2, 1, 1) 170 | self.label_2 = QtGui.QLabel(self.grpMap) 171 | self.label_2.setEnabled(False) 172 | font = QtGui.QFont() 173 | font.setPointSize(11) 174 | self.label_2.setFont(font) 175 | self.label_2.setObjectName("label_2") 176 | self.gridLayout_2.addWidget(self.label_2, 2, 0, 1, 2) 177 | self.fontComboBox = QtGui.QFontComboBox(self.grpMap) 178 | self.fontComboBox.setEnabled(False) 179 | font = QtGui.QFont() 180 | font.setPointSize(11) 181 | self.fontComboBox.setFont(font) 182 | self.fontComboBox.setObjectName("fontComboBox") 183 | self.gridLayout_2.addWidget(self.fontComboBox, 3, 0, 1, 3) 184 | self.textLabel4_2.setBuddy(self.format) 185 | self.textLabel4_3.setBuddy(self.format) 186 | self.textLabel4.setBuddy(self.format) 187 | 188 | self.retranslateUi(ImageExport) 189 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), ImageExport.accept) 190 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), ImageExport.reject) 191 | QtCore.QMetaObject.connectSlotsByName(ImageExport) 192 | 193 | def retranslateUi(self, ImageExport): 194 | ImageExport.setWindowTitle(QtGui.QApplication.translate("ImageExport", "Quantumnik - Image Export", None, QtGui.QApplication.UnicodeUTF8)) 195 | self.groupBox.setTitle(QtGui.QApplication.translate("ImageExport", "Image Output", None, QtGui.QApplication.UnicodeUTF8)) 196 | self.image_output_path.setToolTip(QtGui.QApplication.translate("ImageExport", "Name for the map file to be created from the QGIS project file", None, QtGui.QApplication.UnicodeUTF8)) 197 | self.image_output_button.setText(QtGui.QApplication.translate("ImageExport", "Save As...", None, QtGui.QApplication.UnicodeUTF8)) 198 | self.textLabel4_2.setText(QtGui.QApplication.translate("ImageExport", "Resolution:", None, QtGui.QApplication.UnicodeUTF8)) 199 | self.textLabel4_3.setText(QtGui.QApplication.translate("ImageExport", "(ppi)", None, QtGui.QApplication.UnicodeUTF8)) 200 | self.textLabel4.setText(QtGui.QApplication.translate("ImageExport", "Format:", None, QtGui.QApplication.UnicodeUTF8)) 201 | self.format.setItemText(0, QtGui.QApplication.translate("ImageExport", "png", None, QtGui.QApplication.UnicodeUTF8)) 202 | self.format.setItemText(1, QtGui.QApplication.translate("ImageExport", "jpeg", None, QtGui.QApplication.UnicodeUTF8)) 203 | self.format.setItemText(2, QtGui.QApplication.translate("ImageExport", "pdf", None, QtGui.QApplication.UnicodeUTF8)) 204 | self.format.setItemText(3, QtGui.QApplication.translate("ImageExport", "svg", None, QtGui.QApplication.UnicodeUTF8)) 205 | self.format.setItemText(4, QtGui.QApplication.translate("ImageExport", "ps", None, QtGui.QApplication.UnicodeUTF8)) 206 | self.tmp_render.setText(QtGui.QApplication.translate("ImageExport", "Render to tmp file...", None, QtGui.QApplication.UnicodeUTF8)) 207 | self.auto_open.setText(QtGui.QApplication.translate("ImageExport", "Open after save...", None, QtGui.QApplication.UnicodeUTF8)) 208 | self.transparent_background.setText(QtGui.QApplication.translate("ImageExport", "Transparent Background", None, QtGui.QApplication.UnicodeUTF8)) 209 | self.grpMap.setTitle(QtGui.QApplication.translate("ImageExport", "Print Options (Not Yet Enabled)", None, QtGui.QApplication.UnicodeUTF8)) 210 | self.label.setText(QtGui.QApplication.translate("ImageExport", "Custom Size:", None, QtGui.QApplication.UnicodeUTF8)) 211 | self.neatline.setText(QtGui.QApplication.translate("ImageExport", "Draw Neatline", None, QtGui.QApplication.UnicodeUTF8)) 212 | self.comboBox.setItemText(0, QtGui.QApplication.translate("ImageExport", "Landscape", None, QtGui.QApplication.UnicodeUTF8)) 213 | self.comboBox.setItemText(1, QtGui.QApplication.translate("ImageExport", "Portrait", None, QtGui.QApplication.UnicodeUTF8)) 214 | self.label_2.setText(QtGui.QApplication.translate("ImageExport", "Text Override:", None, QtGui.QApplication.UnicodeUTF8)) 215 | 216 | -------------------------------------------------------------------------------- /mapnikglobe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springmeyer/quantumnik/f745d0852734379e4e5a3e271a82b60084e96ba1/mapnikglobe.png -------------------------------------------------------------------------------- /mapnikhelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springmeyer/quantumnik/f745d0852734379e4e5a3e271a82b60084e96ba1/mapnikhelp.png -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=quantumnik 3 | qgisMinimumVersion=1.8 4 | description=Mapnik integration with QGIS 5 | version=0.4.1 6 | author=Dane Springmeyer 7 | email=dane@dbsgeo.com 8 | ; optional below 9 | experimental=False 10 | changelog=TODO 11 | tags=mapnik,rendering,tiles 12 | homepage=https://github.com/springmeyer/quantumnik 13 | tracker=https://github.com/springmeyer/quantumnik/issues 14 | repository=https://github.com/springmeyer/quantumnik 15 | icon=mapnikglobe.png -------------------------------------------------------------------------------- /print2pixel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Dane Springmeyer (dbsgeo [ -a- ] gmail.com)" 5 | __copyright__ = "Copyright 2008, Dane Springmeyer" 6 | __version__ = "0.0.1SVN" 7 | __license__ = "GPLv2" 8 | 9 | import optparse 10 | import sys 11 | import copy 12 | import math 13 | import platform 14 | 15 | VERBOSE = False 16 | ROUND_RESULT = True 17 | 18 | POSTSCRIPT_PPI = 72.0 # dpi 19 | OGC_PIXEL = 0.28 # mm 20 | METERS_PER_DEGREE = 6378137 * 2 * math.pi/360 21 | #PLOTTER_MAX_WIDTH = 36 # inches 22 | #POWER_POINT_MAX_DIM = (36,56) 23 | #MS_WORD_MAX_DIM = (11,17) 24 | 25 | # use my default screen dimensions for now... 26 | USE_MACBOOK_RESOLUTION = True 27 | 28 | def ppi2mm_px_size(ppi): 29 | return (1.0/ppi)*25.4 30 | 31 | def mm_px_size2ppi(pixel_size): 32 | return 1.0/(pixel_size/25.4) 33 | 34 | POSTSCRIPT_PIXEL = ppi2mm_px_size(POSTSCRIPT_PPI) # mm 0.35277777777777775 35 | OGC_PPI = mm_px_size2ppi(OGC_PIXEL) # 90.714285714285708 as 1 'dot'/.011 inches 36 | 37 | def ppi2microns(ppi): 38 | """Convert ppi to µm 39 | """ 40 | return 25400.0/ppi 41 | 42 | # 76dpi (postcript) translates to a resolution of 334.21 microns 43 | # http://www.cl.cam.ac.uk/~mgk25/metric-typo/ 44 | def microns2ppi(microns): 45 | """Convert µm to ppi 46 | """ 47 | return 25400.0/microns 48 | 49 | def error(E,msg): 50 | if __name__ == '__main__': 51 | sys.exit('// -- Error: %s' % msg) 52 | else: 53 | raise E(msg) 54 | 55 | def msg(msg): 56 | global VERBOSE 57 | if VERBOSE: 58 | print msg 59 | 60 | # Factors for converting to inches by division 61 | # read this as 1 inch == unit value 62 | inch_eq = { 63 | 'in' : 1, 64 | 'ft': 0.0833333333, 65 | 'yd': 0.0277777778, 66 | 'mi': 1.57828283e-5, 67 | 'm': 0.0254, 68 | 'dm': 0.254, 69 | 'cm': 2.54, 70 | 'mm': 25.4, 71 | 'km': 2.54e-5, 72 | 'um': 25400.0, 73 | 'px': POSTSCRIPT_PPI, 74 | } 75 | upper_inch_eq = dict([(k.upper(), v) for k, v in inch_eq.items()]) 76 | 77 | alias = { 78 | # Imperial 79 | 'inch' : 'in', 80 | 'inches' : 'in', 81 | 'foot' : 'ft', 82 | 'feet' : 'ft', 83 | 'yard' : 'yd', 84 | 'yards' : 'yd', 85 | 'mile' : 'mi', 86 | 'miles' : 'mi', 87 | # Metric 88 | 'microns': 'um', 89 | 'micrometer': 'um', 90 | 'micrometres':'um', 91 | 'µm':'um', 92 | 'millimeter' : 'mm', 93 | 'millimetre' : 'mm', 94 | 'centimeter' : 'cm', 95 | 'centimeters' : 'cm', 96 | 'decimeter' : 'dm', 97 | 'decimeters' : 'dm', 98 | 'meter' : 'm', 99 | 'meters' : 'm', 100 | 'metre' : 'm', 101 | 'metres' : 'm', 102 | 'kilometer' : 'km', 103 | 'kilometers' : 'km', 104 | 'kilometre' : 'km', 105 | 'kilometres' : 'km', 106 | } 107 | upper_alias = dict([(k.upper(), v) for k, v in alias.items()]) 108 | 109 | # Paper sizes by name/shorthand 110 | # in millimetres 111 | iso = { 112 | # iso A series 113 | 'A0': (841,1189), 114 | 'A1': (594,841), 115 | 'A2': (420,594), 116 | 'A3': (297,420), 117 | 'A4': (210,297), 118 | 'A5': (148,210), 119 | 'A6': (105,148), 120 | 'A7': (74,105), 121 | 'A8': (52,74), 122 | 'A9': (37,52), 123 | 'A10': (26,37), 124 | # iso B series 125 | 'B0': (1000,1414), 126 | 'B1': (707,1000), 127 | 'B2': (500,707), 128 | 'B3': (353,500), 129 | 'B4': (250,353), 130 | 'B5': (176,250), 131 | 'B6': (125,176), 132 | 'B7': (88,125), 133 | 'B8': (62,88), 134 | 'B9': (44,62), 135 | 'B10': (31,44), 136 | # iso C series 137 | 'C0': (917,1297), 138 | 'C1': (648,917), 139 | 'C2': (458,648), 140 | 'C3': (324,458), 141 | 'C4': (228,324), 142 | 'C5': (162,229), 143 | 'C6': (114.9,162), 144 | 'C7': (88,114.9), 145 | 'C8': (57,81), 146 | 'C9': (40,57), 147 | 'C10': (28,40), 148 | # DIN 476 (German) 149 | '4A0': (1682,2378), 150 | '2A0': (1189,1682), 151 | # SIS 014711 (Swiss) 152 | 'G5': (169,239), 153 | 'E5': (155,220), 154 | } 155 | 156 | # Japanese 157 | jis = { 158 | 'J0': (1030,1456), 159 | 'J1': (728,1030), 160 | 'J2': (515,728), 161 | 'J3': (364,515), 162 | 'J4': (257,364), 163 | 'J5': (182,257), 164 | 'J6': (128,182), 165 | 'J7': (91,128), 166 | 'J8': (64,91), 167 | 'J9': (45,64), 168 | 'J10': (32,45), 169 | 'J11': (22,32), 170 | 'J12': (16,22), 171 | } 172 | 173 | # inches 174 | ansi = { 175 | 'ANSI-A': (8.5,11), 176 | 'ANSI-B': (11,17), 177 | 'ANSI-C': (17,22), 178 | 'ANSI-D': (22,34), 179 | 'ANSI-E': (34,44), 180 | } 181 | 182 | # inches 183 | north_america = { 184 | 'letter': (8.5,11), 185 | 'carta': (8.5,11), 186 | 'legal': (8.5,14), 187 | 'oficio': (8.5,14), 188 | 'executive': (7.25,10.5), 189 | 'tabloid': (11,17), 190 | 'ledge': (17,11), 191 | 'government-letter': (8,10.5), 192 | 'chilean-legal': (8.5,13), 193 | 'philippine-legal': (8.5,13), 194 | } 195 | 196 | sizes = ( 197 | (north_america,'in'), 198 | (iso,'mm'), 199 | (jis,'mm'), 200 | (ansi,'in'), 201 | ) 202 | 203 | size_dict = {} 204 | size_dict.update(north_america) 205 | size_dict.update(iso) 206 | size_dict.update(jis) 207 | size_dict.update(ansi) 208 | 209 | def get_size_by_name(papername): 210 | """Return the units, width, and height of a given paper size. 211 | 212 | Supports lookup of ISO, Japanese, ANSI, and North American sizes. 213 | """ 214 | u = h = w = None 215 | up,lo = papername.upper().strip('\'"'),papername.lower().strip('\'"') 216 | for size in sizes: 217 | if size[0].has_key(lo): 218 | w,h = size[0][lo] 219 | u = size[1] 220 | elif size[0].has_key(up): 221 | w,h = size[0][up] 222 | u = size[1] 223 | if not w: 224 | error(AttributeError,'Could not find paper size of: %s' % papername) 225 | msg("%s size found, using unit: '%s'; width: '%s'; height: '%s'" % (papername.upper(),u,w,h)) 226 | if u != 'in': 227 | factor = get_to_inch_factor(u) 228 | w,h = w/factor,h/factor 229 | # set u = 'in' since we're now forcing work in inches 230 | u = 'in' 231 | msg("%s equivalent in inches is: %s, %s" % (papername.upper(),w,h)) 232 | else: 233 | msg("No 'to-inch' conversion needed, native paper size units are inches...") 234 | return u,w,h 235 | 236 | def print_scale_relative_to_postscript(ppi,system_assumed_dpi=POSTSCRIPT_PPI): 237 | """Return the scale factor to reduce an image of a given ppi to print at intended resolution. 238 | 239 | Needed on systems where 72 ppi is assumed when an image lacks an embedded dpi/ppi exif tag. 240 | """ 241 | # Is 96 dpi on win32 true? 242 | # Likely to hard to guess... 243 | if os.name == 'nt': 244 | system_assumed_dpi = 96.0 245 | elif platform.uname()[0] == 'Darwin': 246 | pass # 72 assumed on mac os 247 | elif platform.uname()[0] == 'Linux': 248 | pass # need to check on linux... 249 | return system_assumed_dpi/ppi * 100.0 250 | 251 | def get_to_inch_factor(unit): 252 | """Return the conversion factor for calculating an inch equivalent for a given unit. 253 | """ 254 | if inch_eq.has_key(unit): 255 | return inch_eq[unit] 256 | elif upper_inch_eq.has_key(unit): 257 | return inch_eq[upper_inch_eq[unit]] 258 | elif alias.has_key(unit): 259 | return inch_eq[alias[unit]] 260 | elif upper_alias.has_key(unit): 261 | return inch_eq[upper_alias[unit]] 262 | else: 263 | error(AttributeError,'Unknown unit type: %s' % unit) 264 | 265 | def get_px_screen_ppi(pixels_wide=1440,pixels_high=900,screen_width=15.4): 266 | """Return the actual pixels per inch for a given display resolution and width. 267 | """ 268 | pixel_density = math.sqrt(pixels_wide**2 + pixels_high**2)/screen_width 269 | msg("Screen pixel density per inch (ppi): '%s'" % pixel_density) 270 | return pixel_density 271 | 272 | def get_px_for_print_size(unit,print_w,print_h,print_res,res_unit): 273 | """Return the pixel width and height given a target print size and resolution. 274 | """ 275 | # get the conversion factor to inches 276 | factor = get_to_inch_factor(unit) 277 | if not factor == 1: 278 | msg("Conversion factor to inches will be '%s' will be 'in = mm/%s'" % (unit,factor)) 279 | # We accept ppi or pixel size in microns for now 280 | if res_unit == 'microns' or res_unit == 'micrometres' or res_unit == 'µm' or res_unit == 'um': 281 | # convert microns to inches since our print sizes 282 | # are going to be forced into inch units 283 | msg("Setting resolution using micrometres (µm)... to '%s' µm" % print_res) 284 | print_res = microns2ppi(print_res) 285 | msg("Micron value equivalent to '%s' ppi" % print_res) 286 | elif res_unit == 'inches' or res_unit == 'in' or res_unit == 'inch': 287 | microns = ppi2microns(print_res) 288 | msg("Setting resolution using inches... to '%s' ppi" % print_res) 289 | msg("Per inch resolution equivalent to pixel size of '%s' microns" % microns) 290 | else: 291 | error(AttributeError,'Unknown print resolution type: %s' % res_unit) 292 | px_w = print_w/factor*print_res 293 | px_h = print_h/factor*print_res 294 | return px_w,px_h 295 | 296 | def get_pixels(unit,w,h,print_res=300,res_unit='inches',margin=0,layout=None,**kwargs): 297 | """Return the pixel width and height given a target resolution, margin, and layout. 298 | 299 | A wrapper around get_px_for_print_size() that handles margins and aspect ratio. 300 | """ 301 | if margin: 302 | w,h = w-margin,h-margin 303 | msg("Margin requested, dimensions in '%s' now: %s,%s " % (unit,w,h)) 304 | 305 | # Pass of to the function that does the real work 306 | px_w, px_h = get_px_for_print_size(unit,w,h,print_res,res_unit) 307 | if layout: 308 | dim = [px_w, px_h] 309 | dim_copy = copy.copy(dim) 310 | if layout == 'portrait': 311 | dim.sort() 312 | if dim_copy == dim: 313 | msg('Layout already of portrait orientation...') 314 | else: 315 | msg('Switched to Portrait type orientation...') 316 | return tuple(dim) 317 | elif layout == 'landscape': 318 | dim.sort() 319 | dim.reverse() 320 | if dim_copy == dim: 321 | msg('Layout already of landscape orientation...') 322 | else: 323 | msg('Switched to Landscape type orientation...') 324 | return tuple(dim) 325 | else: 326 | return px_w,px_h 327 | 328 | def print_map_by_dimensions(params,**kwargs): 329 | """Return the pixels given user defined dimensions and units. 330 | """ 331 | try: 332 | w,h,unit = params.split(',') 333 | except ValueError: # assume inches for now... 334 | unit = 'in' 335 | w,h = params.split(',') 336 | dx, dy = get_pixels(unit,float(w),float(h),**kwargs) 337 | if ROUND_RESULT: 338 | dx, dy = int(dx),int(dy) 339 | return dx, dy 340 | 341 | def print_map_by_name(papername,**kwargs): 342 | """Return the pixels given a known, named paper size. 343 | """ 344 | unit,w,h = get_size_by_name(papername) 345 | dx, dy = get_pixels(unit,float(w),float(h),**kwargs) 346 | if ROUND_RESULT: 347 | dx, dy = int(dx),int(dy) 348 | return dx, dy 349 | 350 | parser = optparse.OptionParser(usage="""python print2pixel.py [options] 351 | 352 | Usage: 353 | $ python print2pixel.py tabloid -r 300 -u inches -l 354 | $ python print2pixel.py 3,7,in 355 | $ python print2pixel.py letter -u inches -r 76 356 | $ python print2pixel.py letter -u microns -r 334.21 357 | 358 | """) 359 | 360 | parser.add_option('-r', '--resolution', 361 | dest='print_res', type='float', 362 | help='Specify the desired resolution in ppi (pixels per inch) or microns (pixel size)') 363 | parser.add_option('-u', '--units', 364 | dest='res_unit', 365 | help='Specify the resolution units as either inches or microns') 366 | parser.add_option('-m', '--margin', 367 | dest='margin', type='float', 368 | help='Specify the a paper margin in the units of the paper size') 369 | parser.add_option('-l', '--landscape', 370 | action='store_const', const='landscape', dest='layout', 371 | help='Force lanscape orientation') 372 | parser.add_option('-p', '--portrait', 373 | action='store_const', const='portrait', dest='layout', 374 | help='Force portrait orientation') 375 | parser.add_option('-v', '--VERBOSE', default=False, 376 | action='store_true', dest='VERBOSE', 377 | help='VERBOSE debug output') 378 | parser.add_option('-n', '--norounding', 379 | action='store_false', dest='ROUND_RESULT', default=True, 380 | help='Do not return rounded integer result for pixel dimensions') 381 | parser.add_option('-s', '--screen', 382 | action='store_const', const=True, dest='screen_res', 383 | help='Set the --resolution to the PPI of your screen (requires -w and -d flags)') 384 | parser.add_option('-w', '--screenwidth', 385 | dest='screen_width', type='float', 386 | help='Screen width in inches') 387 | parser.add_option('-d', '--displaypixels', 388 | dest='display_res', 389 | help='Display pixels as w,h') 390 | parser.add_option('--render', 391 | action='store_const', const=True, dest='render', 392 | help='Render the result using a nik2img test mapfile') 393 | 394 | if __name__ == '__main__': 395 | (options, args) = parser.parse_args() 396 | 397 | if len(args) < 1: 398 | sys.exit('\nPlease provide a named paper size or a triplet of dimensions and their unit, ie 8.5,11,in \n') 399 | else: 400 | size = args[0] 401 | 402 | if options.print_res and not options.res_unit: 403 | sys.exit('\nPlease provide a unit for the resolution value\n') 404 | 405 | if options.res_unit and not options.print_res: 406 | if not options.screen_res: 407 | sys.exit('\nPlease provide a resolution value in addition to the respective unit\n') 408 | 409 | if options.screen_res: 410 | if USE_MACBOOK_RESOLUTION: 411 | options.print_res = get_px_screen_density() 412 | elif not options.res_unit or not options.screen_width or not options.display_res: 413 | sys.exit('\nPlease provide a screen width in inches, and the display resolution\n') 414 | else: 415 | try: 416 | p_w, p_h = map(float,options.display_res.split(',')) 417 | except ValueError: 418 | sys.exit('Problem setting the display resolution\n') 419 | options.print_res = get_px_screen_density(pixels_wide=p_w,pixels_high=p_h,screen_width=options.screen_width) 420 | 421 | if options.VERBOSE: 422 | VERBOSE = True 423 | print 424 | 425 | if not options.ROUND_RESULT: 426 | ROUND_RESULT = False 427 | 428 | kwargs = {} 429 | for k,v in vars(options).items(): 430 | if v != None: 431 | kwargs[k] = v 432 | 433 | if len(size.split(','))> 1: 434 | dx, dy = print_map_by_dimensions(size, **kwargs) 435 | else: 436 | dx, dy = print_map_by_name(size, **kwargs) 437 | 438 | print '// -- Pixel Width: %s' % dx 439 | print '// -- Pixel Height: %s' % dy 440 | 441 | if options.render: 442 | import nik2img 443 | m = nik2img.Map('tests/mapfile.xml','w-%s_h-%s.png' % (dx,dy),width=dx,height=dy) 444 | m.open() 445 | 446 | -------------------------------------------------------------------------------- /quantumnik.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import tempfile 5 | import resources 6 | import relativism 7 | from qgis.gui import * 8 | from qgis.core import * 9 | from PyQt4.QtGui import * 10 | from PyQt4.QtCore import * 11 | 12 | try: 13 | import mapnik2 as mapnik 14 | except ImportError: 15 | import mapnik 16 | 17 | # repair compatibility with mapnik2 development series 18 | if hasattr(mapnik,'Box2d'): 19 | mapnik.Envelope = mapnik.Box2d 20 | 21 | MAPNIK_VERSION = None 22 | 23 | if hasattr(mapnik,'mapnik_version'): 24 | MAPNIK_VERSION = mapnik.mapnik_version() 25 | 26 | import sync 27 | 28 | # Use pdb for debugging 29 | #import pdb 30 | # These lines allow you to set a breakpoint in the app 31 | #pyqtRemoveInputHook() 32 | #pdb.set_trace() 33 | 34 | #TODO - support for Composer 35 | # http://trac.osgeo.org/qgis/changeset/13361 36 | 37 | 38 | try: 39 | from pygments import highlight 40 | from pygments.lexers import XmlLexer 41 | from pygments.formatters import HtmlFormatter 42 | HIGHLIGHTING = True 43 | except: 44 | HIGHLIGHTING = False 45 | 46 | from imageexport import ImageExport 47 | from text_editor import TextEditor 48 | 49 | NAME = 'Quantumnik' 50 | 51 | 52 | class Quantumnik(QObject): 53 | def __init__(self, iface): 54 | QObject.__init__(self) 55 | self.iface = iface 56 | self.canvas = iface.mapCanvas() 57 | 58 | # Fake canvas to use in tab to overlay the quantumnik layer 59 | self.qCanvas = None 60 | self.qCanvasPan = None 61 | self.qCanvasZoomIn = None 62 | self.qCanvasZoomOut = None 63 | self.tabWidget = None 64 | 65 | self.mapnik_map = None 66 | self.using_mapnik = False 67 | self.from_mapfile = False 68 | self.loaded_mapfile = None 69 | self.been_warned = False 70 | self.last_image_path = None 71 | self.dock_window = None 72 | self.keyAction = None 73 | self.keyAction2 = None 74 | self.keyAction3 = None 75 | 76 | def initGui(self): 77 | self.action = QAction(QIcon(":/mapnikglobe.png"), QString("Create Mapnik Canvas"), 78 | self.iface.mainWindow()) 79 | self.action.setWhatsThis("Create Mapnik Canvas") 80 | self.action.setStatusTip("%s: render with Mapnik" % NAME) 81 | QObject.connect(self.action, SIGNAL("triggered()"), self.toggle) 82 | 83 | self.action4 = QAction(QString("View live xml"), self.iface.mainWindow()) 84 | QObject.connect(self.action4, SIGNAL("triggered()"), self.view_xml) 85 | 86 | self.action3 = QAction(QString("Export Mapnik xml"), 87 | self.iface.mainWindow()) 88 | QObject.connect(self.action3, SIGNAL("triggered()"), self.save_xml) 89 | 90 | self.action5 = QAction(QString("Load Mapnik xml"), 91 | self.iface.mainWindow()) 92 | QObject.connect(self.action5, SIGNAL("triggered()"), self.load_xml) 93 | 94 | self.action6 = QAction(QString("Load Cascadenik mml"), 95 | self.iface.mainWindow()) 96 | QObject.connect(self.action6, SIGNAL("triggered()"), self.load_mml) 97 | 98 | self.action7 = QAction(QString("Export Map Graphics"), self.iface.mainWindow()) 99 | QObject.connect(self.action7, SIGNAL("triggered()"), 100 | self.export_image_gui) 101 | 102 | self.helpaction = QAction(QIcon(":/mapnikhelp.png"),"About", 103 | self.iface.mainWindow()) 104 | self.helpaction.setWhatsThis("%s Help" % NAME) 105 | QObject.connect(self.helpaction, SIGNAL("triggered()"), self.helprun) 106 | 107 | self.iface.addToolBarIcon(self.action) 108 | 109 | 110 | self.iface.addPluginToMenu("&%s" % NAME, self.action) 111 | self.iface.addPluginToMenu("&%s" % NAME, self.helpaction) 112 | self.iface.addPluginToMenu("&%s" % NAME, self.action3) 113 | self.iface.addPluginToMenu("&%s" % NAME, self.action4) 114 | self.iface.addPluginToMenu("&%s" % NAME, self.action5) 115 | self.iface.addPluginToMenu("&%s" % NAME, self.action6) 116 | self.iface.addPluginToMenu("&%s" % NAME, self.action7) 117 | 118 | # > QGIS 1.2 119 | if hasattr(self.iface,'registerMainWindowAction'): 120 | self.keyAction2 = QAction(QString("Switch to QGIS"), self.iface.mainWindow()) 121 | self.iface.registerMainWindowAction(self.keyAction2, "Ctrl+[") 122 | self.iface.addPluginToMenu("&%s" % NAME, self.keyAction2) 123 | QObject.connect(self.keyAction2, SIGNAL("triggered()"),self.switch_tab_qgis) 124 | 125 | self.keyAction3 = QAction(QString("Switch to Mapnik"), self.iface.mainWindow()) 126 | self.iface.registerMainWindowAction(self.keyAction3, "Ctrl+]") 127 | self.iface.addPluginToMenu("&%s" % NAME, self.keyAction3) 128 | QObject.connect(self.keyAction3, SIGNAL("triggered()"),self.switch_tab_mapnik) 129 | 130 | def unload(self): 131 | self.iface.removePluginMenu("&%s" % NAME,self.action) 132 | self.iface.removePluginMenu("&%s" % NAME,self.helpaction) 133 | self.iface.removePluginMenu("&%s" % NAME,self.action3) 134 | self.iface.removePluginMenu("&%s" % NAME,self.action4) 135 | self.iface.removePluginMenu("&%s" % NAME,self.action5) 136 | self.iface.removePluginMenu("&%s" % NAME,self.action6) 137 | self.iface.removePluginMenu("&%s" % NAME,self.action7) 138 | self.iface.removeToolBarIcon(self.action) 139 | if self.keyAction: 140 | self.iface.unregisterMainWindowAction(self.keyAction) 141 | if self.keyAction2: 142 | self.iface.unregisterMainWindowAction(self.keyAction2) 143 | if self.keyAction3: 144 | self.iface.unregisterMainWindowAction(self.keyAction3) 145 | 146 | def export_image_gui(self): 147 | flags = Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.WindowStaysOnTopHint 148 | export = ImageExport(self,flags) 149 | export.show() 150 | 151 | def view_xml(self,m=None): 152 | if not self.dock_window: 153 | self.dock_window = TextEditor(self) 154 | self.iface.mainWindow().addDockWidget( Qt.BottomDockWidgetArea, 155 | self.dock_window ) 156 | if not self.using_mapnik: 157 | # http://trac.osgeo.org/qgis/changeset/12955 - render starting signal 158 | QObject.connect(self.canvas, SIGNAL("renderComplete(QPainter *)"), 159 | self.checkLayers) 160 | 161 | self.dock_window.show() 162 | if self.loaded_mapfile: 163 | # if we have loaded a map xml or mml 164 | # so lets just display the active file 165 | xml = open(self.loaded_mapfile,'rb').read() 166 | else: 167 | if not m: 168 | # regenerate from qgis objects 169 | e_c = sync.EasyCanvas(self,self.canvas) 170 | m = e_c.to_mapnik() 171 | if hasattr(mapnik,'save_map_to_string'): 172 | xml = mapnik.save_map_to_string(m) 173 | else: 174 | (handle, mapfile) = tempfile.mkstemp('.xml', 'quantumnik-map-') 175 | os.close(handle) 176 | mapnik.save_map(m,str(mapfile)) 177 | xml = open(mapfile,'rb').read() 178 | e = self.canvas.extent() 179 | bbox = '%s %s %s %s' % (e.xMinimum(),e.yMinimum(), 180 | e.xMaximum(),e.yMaximum()) 181 | cmd = '\n\n' % (self.canvas.width(), self.canvas.height(), bbox) 182 | try: 183 | if self.mapnik_map: 184 | cmd += '\n' % (self.mapnik_map.scale_denominator()) 185 | except: 186 | pass 187 | 188 | code = xml + cmd 189 | if HIGHLIGHTING: 190 | highlighted = highlight(code, XmlLexer(), HtmlFormatter(linenos=False, nowrap=False, full=True)) 191 | self.dock_window.textEdit.setHtml(highlighted) 192 | else: 193 | self.dock_window.textEdit.setText(xml + cmd) 194 | 195 | def helprun(self): 196 | infoString = QString("Written by Dane Springmeyer\nhttps://github.com/springmeyer/quantumnik") 197 | QMessageBox.information(self.iface.mainWindow(),"About %s" % NAME,infoString) 198 | 199 | def toggle(self): 200 | if self.using_mapnik: 201 | self.stop_rendering() 202 | else: 203 | self.start_rendering() 204 | 205 | def proj_warning(self): 206 | self.been_warned = True 207 | ren = self.canvas.mapRenderer() 208 | if not ren.hasCrsTransformEnabled() and self.canvas.layerCount() > 1: 209 | if hasattr(self.canvas.layer(0),'crs'): 210 | if not self.canvas.layer(0).crs().toProj4() == ren.destinationCrs().toProj4(): 211 | QMessageBox.information(self.iface.mainWindow(),"Warning","The projection of the map and the first layer do not match. Mapnik may not render the layer(s) correctly.\n\nYou likely need to either enable 'On-the-fly' CRS transformation or set the Map projection in your Project Properties to the projection of your layer(s).") 212 | else: 213 | if not self.canvas.layer(0).srs().toProj4() == ren.destinationSrs().toProj4(): 214 | QMessageBox.information(self.iface.mainWindow(),"Warning","The projection of the map and the first layer do not match. Mapnik may not render the layer(s) correctly.\n\nYou likely need to either enable 'On-the-fly' CRS transformation or set the Map projection in your Project Properties to the projection of your layer(s).") 215 | 216 | def save_xml(self): 217 | # need to expose as an option! 218 | relative_paths = True 219 | 220 | mapfile = QFileDialog.getSaveFileName(None, "Save file dialog", 221 | 'mapnik.xml', "Mapfile (*.xml)") 222 | if mapfile: 223 | e_c = sync.EasyCanvas(self,self.canvas) 224 | mapfile_ = str(mapfile) 225 | base_path = os.path.dirname(mapfile_) 226 | e_c.base_path = base_path 227 | m = e_c.to_mapnik() 228 | mapnik.save_map(m,mapfile_) 229 | 230 | if relative_paths: 231 | relativism.fix_paths(mapfile_,base_path) 232 | 233 | def make_bundle(self): pass 234 | # todo: accept directory name 235 | # move mapfile and all file based datasources 236 | # into that folder and stash some docs inside 237 | # provide option to zip and upload to url on the fly 238 | 239 | def set_canvas_from_mapnik(self): 240 | # set up keyboard shortcut 241 | # > QGIS 1.2 242 | if hasattr(self.iface,'registerMainWindowAction'): 243 | if not self.keyAction: 244 | # TODO - hotkey does not work on linux.... 245 | self.keyAction = QAction(QString("Refresh " + NAME), self.iface.mainWindow()) 246 | self.iface.registerMainWindowAction(self.keyAction, "Ctrl+r") 247 | self.iface.addPluginToMenu("&%s" % NAME, self.keyAction) 248 | QObject.connect(self.keyAction, SIGNAL("triggered()"),self.toggle) 249 | 250 | self.mapnik_map.zoom_all() 251 | e = self.mapnik_map.envelope() 252 | crs = QgsCoordinateReferenceSystem() 253 | srs = self.mapnik_map.srs 254 | if srs == '+init=epsg:900913': 255 | # until we can look it up in srs.db... 256 | merc = "+init=EPSG:900913" 257 | crs.createFromProj4(QString(merc)) 258 | elif 'init' in srs: 259 | # TODO - quick hack, needs regex and fallbacks 260 | epsg = srs.split(':')[1] 261 | crs.createFromEpsg(int(epsg)) 262 | else: 263 | if srs == '+proj=latlong +datum=WGS84': 264 | # expand the Mapnik srs a bit 265 | # http://trac.mapnik.org/ticket/333 266 | srs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' 267 | crs.createFromProj4(QString(srs)) 268 | if hasattr(self.canvas.mapRenderer(),'setDestinationCrs'): 269 | self.canvas.mapRenderer().setDestinationCrs(crs) 270 | else: 271 | self.canvas.mapRenderer().setDestinationSrs(crs) 272 | if not crs.isValid(): 273 | QMessageBox.information(self.iface.mainWindow(), 274 | "Warning","Projection not understood") 275 | return 276 | QObject.connect(self.canvas, SIGNAL("renderComplete(QPainter *)"), 277 | self.render_dynamic) 278 | self.canvas.setExtent(QgsRectangle(e.minx,e.miny,e.maxx,e.maxy)) 279 | self.canvas.refresh() 280 | 281 | def set_mapnik_to_canvas(self): 282 | QObject.connect(self.canvas, SIGNAL("renderComplete(QPainter *)"), 283 | self.render_dynamic) 284 | self.canvas.refresh() 285 | 286 | def refresh_loaded_mapfile(self): 287 | if self.mapfile_format == 'Cascadenik mml': 288 | self.load_mml(refresh=True) 289 | else: 290 | self.load_xml(refresh=True) 291 | 292 | def load_mml(self,refresh=False): 293 | self.from_mapfile = True 294 | self.mapfile_format = 'Cascadenik mml' 295 | if self.loaded_mapfile and refresh: 296 | mapfile = self.loaded_mapfile 297 | else: 298 | mapfile = QFileDialog.getOpenFileName(None, "Open file dialog", 299 | '', "Cascadenik MML (*.mml)") 300 | if mapfile: 301 | self.mapnik_map = mapnik.Map(1,1) 302 | import cascadenik 303 | if hasattr(cascadenik,'VERSION'): 304 | major = int(cascadenik.VERSION.split('.')[0]) 305 | if major < 1: 306 | from cascadenik import compile 307 | compiled = '%s_compiled.xml' % os.path.splitext(str(mapfile))[0] 308 | open(compiled, 'w').write(compile(str(mapfile))) 309 | mapnik.load_map(self.mapnik_map, compiled) 310 | elif major == 1: 311 | output_dir = os.path.dirname(str(mapfile)) 312 | cascadenik.load_map(self.mapnik_map,str(mapfile),output_dir,verbose=False) 313 | elif major > 1: 314 | raise NotImplementedError('This nik2img version does not yet support Cascadenik > 1.x, please upgrade nik2img to the latest release') 315 | else: 316 | from cascadenik import compile 317 | compiled = '%s_compiled.xml' % os.path.splitext(str(mapfile))[0] 318 | #if os.path.exits(compiled): 319 | #pass 320 | open(compiled, 'w').write(compile(str(mapfile))) 321 | mapnik.load_map(self.mapnik_map, compiled) 322 | 323 | if self.loaded_mapfile and refresh: 324 | self.set_mapnik_to_canvas() 325 | else: 326 | self.set_canvas_from_mapnik() 327 | self.loaded_mapfile = str(mapfile) 328 | 329 | def load_xml(self,refresh=False): 330 | # TODO - consider putting into its own layer: 331 | # https://trac.osgeo.org/qgis/ticket/2392#comment:4 332 | self.from_mapfile = True 333 | self.mapfile_format = 'xml mapfile' 334 | if self.loaded_mapfile and refresh: 335 | mapfile = self.loaded_mapfile 336 | else: 337 | mapfile = QFileDialog.getOpenFileName(None, "Open file dialog", 338 | '', "XML Mapfile (*.xml)") 339 | if mapfile: 340 | self.mapnik_map = mapnik.Map(1,1) 341 | mapnik.load_map(self.mapnik_map,str(mapfile)) 342 | if self.loaded_mapfile and refresh: 343 | self.set_mapnik_to_canvas() 344 | else: 345 | self.set_canvas_from_mapnik() 346 | self.loaded_mapfile = str(mapfile) 347 | 348 | def finishStopRender(self): 349 | self.iface.mapCanvas().setMinimumSize(QSize(0, 0)) 350 | 351 | def stop_rendering(self): 352 | # Disconnect all the signals as we disable the tool 353 | QObject.disconnect(self.qCanvas, SIGNAL("renderComplete(QPainter *)"), 354 | self.render_dynamic) 355 | QObject.disconnect(self.qCanvas, 356 | SIGNAL("xyCoordinates(const QgsPoint&)"), 357 | self.updateCoordsDisplay) 358 | QObject.disconnect(self.canvas, SIGNAL("renderComplete(QPainter *)"), 359 | self.checkLayers) 360 | QObject.disconnect(self.canvas, SIGNAL("extentsChanged()"), 361 | self.checkExtentsChanged) 362 | QObject.disconnect(self.canvas, SIGNAL("mapToolSet(QgsMapTool *)"), 363 | self.mapToolSet) 364 | self.using_mapnik = False 365 | # If the current tab is quantumnik then we need to update the extent 366 | # of the main map when exiting to make sure they are in sync 367 | if self.tabWidget.currentIndex() == 1: 368 | self.mapnikMapCoordChange() 369 | # Need to restore the main map instead of the mapnik tab 370 | tabWidgetSize = self.tabWidget.size() 371 | mapCanvasExtent = self.iface.mapCanvas().extent() 372 | self.iface.mapCanvas().setMinimumSize(tabWidgetSize) 373 | self.iface.mainWindow().setCentralWidget(self.iface.mapCanvas()) 374 | self.iface.mapCanvas().show() 375 | # Set the canvas extent to the same place it was before getting 376 | # rid of the tabs 377 | self.iface.mapCanvas().setExtent(mapCanvasExtent) 378 | self.canvas.refresh() 379 | 380 | # null out some vars 381 | self.qCanvasPan = None 382 | self.qCanvasZoomIn = None 383 | self.qCanvasZoomOut = None 384 | # We have to let the main app swizzle the screen and then 385 | # hammer it back to the size we want 386 | QTimer.singleShot(1, self.finishStopRender) 387 | 388 | 389 | def create_mapnik_map(self): 390 | if not self.been_warned: 391 | self.proj_warning() 392 | self.easyCanvas = sync.EasyCanvas(self,self.canvas) 393 | self.mapnik_map = self.easyCanvas.to_mapnik() 394 | if self.dock_window: 395 | self.view_xml(self.mapnik_map) 396 | 397 | @property 398 | def background(self): 399 | return sync.css_color(self.canvas.backgroundBrush().color()) 400 | 401 | # Provide a hack to try and find the map coordinate status bar element 402 | # to take over while the mapnik canvas is in play. 403 | def findMapCoordsStatus(self): 404 | coordStatusWidget = None 405 | sb = self.iface.mainWindow().statusBar() 406 | for x in sb.children(): 407 | # Check if we have a line edit 408 | if isinstance(x, QLineEdit): 409 | # Now check if the text does not contain a ':' 410 | if not ':' in x.text(): 411 | # we have our coord status widget 412 | coordStatusWidget = x 413 | return coordStatusWidget 414 | 415 | def finishStartRendering(self): 416 | self.tabWidget.setMinimumSize(QSize(0, 0)) 417 | self.canvas.refresh() 418 | 419 | def start_rendering(self): 420 | if self.from_mapfile and not self.canvas.layerCount(): 421 | self.refresh_loaded_mapfile() 422 | else: 423 | self.from_mapfile = False 424 | # http://trac.osgeo.org/qgis/changeset/12926 425 | # TODO - if not dirty we don't need to create a new map from scratch... 426 | self.create_mapnik_map() 427 | # Need to create a tab widget to toss into the main window 428 | # to hold both the main canvas as well as the mapnik rendering 429 | mapCanvasSize = self.canvas.size() 430 | mapCanvasExtent = self.iface.mapCanvas().extent() 431 | newWidget = QTabWidget(self.iface.mainWindow()) 432 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, 433 | QSizePolicy.Preferred) 434 | sizePolicy.setHorizontalStretch(0) 435 | sizePolicy.setVerticalStretch(0) 436 | sizePolicy.setHeightForWidth(newWidget.sizePolicy().hasHeightForWidth()) 437 | newWidget.setSizePolicy(sizePolicy) 438 | newWidget.setSizeIncrement(QSize(0, 0)) 439 | newWidget.setBaseSize(mapCanvasSize) 440 | newWidget.resize(mapCanvasSize) 441 | # Very important: Set the min size of the tabs to the size of the 442 | # original canvas. We will then let the main app take control 443 | # and then use a one shot timer to set the min size back down. It 444 | # is a hack, but allows us to keep the canvas and tab size correct. 445 | newWidget.setMinimumSize(mapCanvasSize) 446 | 447 | # This is the new blank canvas that we will use the qpainter 448 | # from to draw the mapnik image over. 449 | self.qCanvas = QgsMapCanvas(self.iface.mainWindow()) 450 | self.qCanvas.setCanvasColor(QColor(255,255,255)) 451 | self.qCanvas.enableAntiAliasing(True) 452 | self.qCanvas.useImageToRender(False) 453 | self.qCanvas.show() 454 | 455 | # A set of map tools for the mapnik canvas 456 | self.qCanvasPan = QgsMapToolPan(self.qCanvas) 457 | self.qCanvasZoomIn = QgsMapToolZoom(self.qCanvas,False) 458 | self.qCanvasZoomOut = QgsMapToolZoom(self.qCanvas,True) 459 | self.mapToolSet(self.canvas.mapTool()) 460 | 461 | # Add the canvas items to the tabs 462 | newWidget.addTab(self.canvas, "Main Map") 463 | newWidget.addTab(self.qCanvas, "Mapnik Rendered Map") 464 | self.tabWidget = newWidget 465 | # Add the tabs as the central widget 466 | self.iface.mainWindow().setCentralWidget(newWidget) 467 | # Need to set the extent of both canvases as we have just resized 468 | # things 469 | self.canvas.setExtent(mapCanvasExtent) 470 | self.qCanvas.setExtent(mapCanvasExtent) 471 | # Hook up to the tabs changing so we can make sure to update the 472 | # rendering in a lazy way... i.e. a pan in the main canvas will 473 | # not cause a redraw in the mapnik tab until the mapnik tab 474 | # is selected. 475 | self.connect(self.tabWidget,SIGNAL("currentChanged(int)"), 476 | self.tabChanged) 477 | # Grab the maptool change signal so the mapnik canvas tool 478 | # can stay in sync. 479 | # TODO: We need to get the in/out property for the zoom tool 480 | # exposed to the python bindings. As it stands now, we can 481 | # not tell what direction the tool is going when we get this 482 | # signal and it is a zoom tool. 483 | QObject.connect(self.canvas, SIGNAL("mapToolSet(QgsMapTool *)"), 484 | self.mapToolSet) 485 | # Catch any mouse movements over the mapnik canvas and 486 | # sneek in and update the cord display 487 | ## This is a hack to find the status element to populate with xy 488 | self.mapCoords = self.findMapCoordsStatus() 489 | QObject.connect(self.qCanvas, 490 | SIGNAL("xyCoordinates(const QgsPoint&)"), 491 | self.updateCoordsDisplay) 492 | # Get the renderComplete signal for the qCanvas to allow us to 493 | # render the mapnik image over it. 494 | QObject.connect(self.qCanvas, SIGNAL("renderComplete(QPainter *)"), 495 | self.render_dynamic) 496 | # Get the renderComplete signal for the main canvas so we can tell 497 | # if there have been any layer changes and if we need to re-draw 498 | # the mapnik image. This is mainly for when the mapnik tab is 499 | # active but layer changes are happening. 500 | QObject.connect(self.canvas, SIGNAL("renderComplete(QPainter *)"), 501 | self.checkLayers) 502 | QObject.connect(self.canvas, SIGNAL("extentsChanged()"), 503 | self.checkExtentsChanged) 504 | self.using_mapnik=True 505 | # We use a single shot timer to let the main app resize the main 506 | # window with us holding a minsize we want, then we reset the 507 | # allowable min size after the main app has its turn. Hack, but 508 | # allows for the window to be rezised with a new main widget. 509 | QTimer.singleShot(1, self.finishStartRendering) 510 | 511 | def updateCoordsDisplay(self, p): 512 | if self.mapCoords: 513 | capturePyString = "%.5f,%.5f" % (p.x(),p.y()) 514 | capture_string = QString(capturePyString) 515 | self.mapCoords.setText(capture_string) 516 | 517 | def mapToolSet(self, tool): 518 | # something changed here in recent QGIS versions causing: 519 | # exceptions when closing QGIS because these objects are None 520 | if tool: 521 | if isinstance(tool,QgsMapToolPan): 522 | self.qCanvas.setMapTool(self.qCanvasPan) 523 | elif isinstance(tool,QgsMapToolZoom): 524 | # Yet another hack to find out if the tool we are using is a 525 | # zoom in or out 526 | if tool.action().text() == QString("Zoom In"): 527 | self.qCanvas.setMapTool(self.qCanvasZoomIn) 528 | else: 529 | self.qCanvas.setMapTool(self.qCanvasZoomOut) 530 | else: 531 | self.qCanvas.setMapTool(self.qCanvasPan) 532 | 533 | def switch_tab_qgis(self): 534 | if self.tabWidget: 535 | self.tabWidget.setCurrentIndex(0) 536 | 537 | def switch_tab_mapnik(self): 538 | if self.tabWidget: 539 | self.tabWidget.setCurrentIndex(1) 540 | 541 | def tabChanged(self, index): 542 | if index == 0: 543 | self.mapnikMapCoordChange() 544 | else: 545 | self.mainMapCoordChange() 546 | 547 | def mainMapCoordChange(self): 548 | # print "coordChange" 549 | self.mapnik_map = self.easyCanvas.to_mapnik(self.mapnik_map) 550 | self.qCanvas.setExtent(self.iface.mapCanvas().extent()) 551 | self.qCanvas.refresh() 552 | 553 | def mapnikMapCoordChange(self): 554 | # print "coordChange" 555 | self.canvas.setExtent(self.qCanvas.extent()) 556 | self.canvas.refresh() 557 | 558 | # Here we are checking to see if we got a new extent on the main 559 | # canvas even though we are in the mapnik tab... in that case we have 560 | # done something like zoom to full extent etc. 561 | def checkExtentsChanged(self): 562 | if self.tabWidget: 563 | if self.tabWidget.currentIndex() == 1: 564 | self.mainMapCoordChange() 565 | 566 | 567 | # Here we are checking to see if we got a render complete on the main 568 | # canvas even though we are in the mapnik tab... in that case we have 569 | # a new layer etc. 570 | def checkLayers(self, painter=None): 571 | if self.tabWidget: 572 | if self.tabWidget.currentIndex() == 1: 573 | # There was a change in the main canvas while we are viewing 574 | # the mapnik canvas (i.e. layer added/removed etc) so we 575 | # need to refresh the mapnik map 576 | self.mapnik_map = self.easyCanvas.to_mapnik(self.mapnik_map) 577 | self.qCanvas.refresh() 578 | # We also make sure the main map canvas gets put back to the 579 | # current extent of the qCanvas incase the main map got changed 580 | # as a side effect since updates to it are lazy loaded on tab 581 | # change. 582 | self.canvas.setExtent(self.qCanvas.extent()) 583 | # We make sure to update the XML viewer if 584 | # if is open 585 | if self.dock_window: 586 | self.view_xml(self.mapnik_map) 587 | if self.dock_window: 588 | self.view_xml() 589 | 590 | def render_dynamic(self, painter): 591 | if self.mapnik_map: 592 | w = painter.device().width() 593 | h = painter.device().height() 594 | # using canvas dims leads to shift in QGIS < 1.3... 595 | #w = self.canvas.width() 596 | #h = self.canvas.height() 597 | try: 598 | self.mapnik_map.resize(w,h) 599 | except: 600 | self.mapnik_map.width = w 601 | self.mapnik_map.height = h 602 | if self.qCanvas: 603 | can = self.qCanvas 604 | else: 605 | can = self.canvas 606 | try: 607 | e = can.extent() 608 | except: 609 | can = self.canvas 610 | e = can.extent() 611 | bbox = mapnik.Envelope(e.xMinimum(),e.yMinimum(), 612 | e.xMaximum(),e.yMaximum()) 613 | self.mapnik_map.zoom_to_box(bbox) 614 | im = mapnik.Image(w,h) 615 | mapnik.render(self.mapnik_map,im) 616 | if os.name == 'nt': 617 | qim = QImage() 618 | qim.loadFromData(QByteArray(im.tostring('png'))) 619 | painter.drawImage(0,0,qim) 620 | else: 621 | qim = QImage(im.tostring(),w,h,QImage.Format_ARGB32) 622 | painter.drawImage(0,0,qim.rgbSwapped()) 623 | can.refresh() 624 | -------------------------------------------------------------------------------- /relativism.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | from xml.dom import minidom 6 | from os.path import abspath, dirname, normcase, normpath, splitdrive 7 | 8 | # http://code.activestate.com/recipes/302594/ 9 | 10 | def commonpath(a, b): 11 | """Returns the longest common to 'paths' path. 12 | 13 | Unlike the strange os.path.commonprefix: 14 | - this returns valid path 15 | - accepts only two arguments 16 | """ 17 | a = normpath(normcase(a)) 18 | b = normpath(normcase(b)) 19 | 20 | if a == b: 21 | return a 22 | 23 | while len(a) > 0: 24 | if a == b: 25 | return a 26 | 27 | if len(a) > len(b): 28 | a = dirname(a) 29 | else: 30 | b = dirname(b) 31 | 32 | return None 33 | 34 | 35 | def relpath(target, base_path=os.curdir): 36 | """ 37 | Return a relative path to the target from either the current directory 38 | or an optional base directory. 39 | 40 | Base can be a directory specified either as absolute or relative 41 | to current directory. 42 | """ 43 | 44 | base_path = normcase(abspath(normpath(base_path))) 45 | target = normcase(abspath(normpath(target))) 46 | 47 | if base_path == target: 48 | return '.' 49 | 50 | # On the windows platform the target may be on a different drive. 51 | if splitdrive(base_path)[0] != splitdrive(target)[0]: 52 | return None 53 | 54 | common_path_len = len(commonpath(base_path, target)) 55 | 56 | # If there's no common prefix decrease common_path_len should be less by 1 57 | base_drv, base_dir = splitdrive(base_path) 58 | if common_path_len == len(base_drv) + 1: 59 | common_path_len -= 1 60 | 61 | # if base_path is root directory - no directories up 62 | if base_dir == os.sep: 63 | dirs_up = 0 64 | else: 65 | dirs_up = base_path[common_path_len:].count(os.sep) 66 | 67 | ret = os.sep.join([os.pardir] * dirs_up) 68 | if len(target) > common_path_len: 69 | ret = os.path.join(ret, target[common_path_len + 1:]) 70 | 71 | return ret 72 | 73 | def fix_file_params(name,element,base_path): 74 | params = element.getElementsByTagName(name) 75 | if params: 76 | for param in params: 77 | attr = param.getAttribute('name') 78 | if attr == 'file': 79 | path = param.firstChild.nodeValue 80 | param.firstChild.nodeValue = relpath(path,base_path) 81 | 82 | def fix_sym_file(element,base_path): 83 | path = element.getAttribute('file') 84 | if path: 85 | element.setAttribute('file',relpath(path,base_path)) 86 | 87 | def fix_paths(mapfile,base_path): 88 | doc = minidom.parse(mapfile) 89 | datasources = doc.getElementsByTagName("Datasource") 90 | for ds in datasources: 91 | fix_file_params('Parameter',ds,base_path) 92 | pnt_syms = doc.getElementsByTagName("PointSymbolizer") 93 | for pnt in pnt_syms: 94 | fix_sym_file(pnt,base_path) 95 | 96 | open(mapfile,'wb').write(doc.toxml()) 97 | 98 | #def move_files(mapfile,base_path): pass 99 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | # For release: 2 | # change versions in __init__.py and on distribution site... 3 | # remove '-dev' from name 4 | # update CHANGELOG 5 | # commit changes 6 | # tag with mercurial 7 | # $ hg tag -m "tagging the release" 8 | cd ../ 9 | rm quantumnik.zip 10 | zip -9vr quantumnik.zip quantumnik/* 11 | -------------------------------------------------------------------------------- /remake: -------------------------------------------------------------------------------- 1 | make clean 2 | make 3 | sudo make install 4 | #open -a qgis 5 | #/Applications/Qgis.app/Contents/MacOS/Qgis 6 | -------------------------------------------------------------------------------- /render_wrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | try: 6 | import mapnik2 as mapnik 7 | except ImportError: 8 | import mapnik 9 | 10 | def render_to_file(mapnik_map,output,format): 11 | 12 | # get the full path for a users directory 13 | if '~' in output: 14 | output = os.path.expanduser(output) 15 | 16 | # mapnik won't create directories so 17 | # we have to make sure they exist first... 18 | dirname = os.path.dirname(output) 19 | if not os.path.exists(dirname): 20 | os.makedirs(dirname) 21 | # render out to the desired format 22 | if format in ('png','png256','jpeg') or (hasattr(mapnik,'mapnik_version') and mapnik.mapnik_version() >= 700): 23 | try: 24 | mapnik.render_to_file(mapnik_map,output,format) 25 | except Exception, e: 26 | return (False,e) 27 | else: 28 | try: 29 | import cairo 30 | surface = getattr(cairo,'%sSurface' % format.upper())(output,mapnik_map.width,mapnik_map.height) 31 | mapnik.render(mapnik_map, surface) 32 | surface.finish() 33 | except Exception, e: 34 | return (False,e) 35 | return (True,mapnik_map) -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | mapnikglobe.png 4 | mapnikhelp.png 5 | 6 | 7 | -------------------------------------------------------------------------------- /sync.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import re 5 | import math 6 | import tempfile 7 | from qgis.gui import * 8 | from qgis.core import * 9 | from PyQt4.QtGui import * 10 | from PyQt4.QtCore import * 11 | from quantumnik import MAPNIK_VERSION 12 | 13 | try: 14 | import mapnik2 as mapnik 15 | except ImportError: 16 | import mapnik 17 | 18 | #import pdb 19 | #pyqtRemoveInputHook() 20 | #pdb.set_trace() 21 | 22 | # TODO - support composer 23 | # http://trac.osgeo.org/qgis/changeset/12372 24 | 25 | MAPNIK_PLUGINS = None 26 | 27 | if MAPNIK_VERSION: 28 | MAPNIK_PLUGINS = list(mapnik.DatasourceCache.plugin_names()) 29 | 30 | # warn once about layers we cannot yet read... 31 | INCOMPATIBLE_LAYER_WARNING = True 32 | 33 | # warn once about plugins Mapnik does not have... 34 | MISSING_PLUGIN_WARNING = True 35 | 36 | # warn once that we don't yet support symbology-ng (aka rendererV2) 37 | INCOMPATIBLE_RENDERER_WARNING = True 38 | 39 | def is_number(s): 40 | """ Test if the value can be converted to a number. 41 | """ 42 | try: 43 | float(s) 44 | return True 45 | except ValueError: 46 | return False 47 | 48 | def check_plug(f): 49 | msg = "You will see this warning once per session...\n\nSorry your Mapnik version does not include the '%s' plugin.\n This plugin is needed by Mapnik to read the file: %s.\n\n If you downloaded a Mapnik installer, please contact the author about adding support for this Mapnik plugin.\n\n If you installed Mapnik from source rebuild Mapnik using the SCons option: INPUT_PLUGINS=%s,%s" 50 | def newf(*_args, **_kwds): 51 | global MISSING_PLUGIN_WARNING 52 | self = _args[0] 53 | if MAPNIK_PLUGINS: 54 | if not f.func_name in MAPNIK_PLUGINS: 55 | if MISSING_PLUGIN_WARNING: 56 | MISSING_PLUGIN_WARNING = False 57 | return self.message(msg % (f.func_name, self.source, f.func_name, ','.join(MAPNIK_PLUGINS))) 58 | else: 59 | return None 60 | return f(*_args, **_kwds) 61 | return newf 62 | 63 | def get_variant_value(variant): 64 | """'BitArray', 'Bitmap', 'Bool', 'Brush', 'ByteArray', 'Char', 'Color', 'Cursor', 'Date', 'DateTime', 'Double', 'Font', 'Icon', 'Image', 'Int', 'Invalid', 'KeySequence', 'Line', 'LineF', 'List', 'Locale', 'LongLong', 'Map', 'Matrix', 'Palette', 'Pen', 'Pixmap', 'Point', 'PointF', 'Polygon', 'Rect', 'RectF', 'RegExp', 'Region', 'Size', 'SizeF', 'SizePolicy', 'String', 'StringList', 'TextFormat', 'TextLength', 'Time', 'Transform', 'Type', 'UInt', 'ULongLong', 'Url', 65 | """ 66 | if variant.type() == QVariant.Double: 67 | return variant.toDouble()[0] 68 | elif variant.type() == QVariant.Int: 69 | return variant.toInt()[0] 70 | elif variant.type() == QVariant.String: 71 | return unicode(variant.toString()) 72 | elif variant.type() == QVariant.Bool: 73 | return variant.toBool()[0] 74 | elif variant.type() == QVariant.StringList: 75 | return variant.toList()[0] 76 | elif variant.type() == QVariant.List: 77 | return variant.toList()[0] 78 | else: 79 | raise TypeError('Field type not understood, value was: %s' % variant.toString()) 80 | 81 | class Ramp(object): 82 | def __init__(self,start,end,min,max): 83 | """ Ramp between two Mapnik Colors given a min and max value (int or float). 84 | """ 85 | self.start = start 86 | self.end = end 87 | self.min = min 88 | self.max = max 89 | 90 | def scale(self,high,low,val): 91 | """ Ramp values by high and low bounds. 92 | """ 93 | a,b = (val - self.min),(self.max - self.min) 94 | c,d = (self.max - val),(self.max - self.min) 95 | if 0 in [a,b,c,d]: 96 | return None 97 | highest = high * a/b 98 | lowest = low * c/d 99 | return int(highest + lowest) 100 | 101 | def color_for_value(self,val): 102 | """ Scale each color by a given value. 103 | """ 104 | red = self.scale(self.start.r,self.end.r,val) or 0 105 | green = self.scale(self.start.g,self.end.g,val) or 0 106 | blue = self.scale(self.start.b,self.end.b,val) or 0 107 | alpha = self.scale(self.start.a,self.end.a,val) or 1 108 | return red,green,blue,alpha 109 | 110 | def css_color(qc): 111 | """ Turn a QColor into a mapnik::color.""" 112 | # note, alpha is usually ignored in Mapnik and 113 | # and is handled in symbolizer 'CssParameter' 114 | # using the 'opacity' parameter 115 | try: 116 | col = mapnik.Color(str('rgba(%s,%s,%s,%s)' % (qc.red(),qc.green(),qc.blue(),qc.alpha()))) 117 | except: 118 | col = mapnik.Color(str('rgb(%s,%s,%s)' % (qc.red(),qc.green(),qc.blue()))) 119 | return col 120 | 121 | def get_cap(cap): 122 | """ Turn a Qt Line Cap enum into a Mapnik one. 123 | """ 124 | # TODO - convert into a dictionary dispatch 125 | # with proper default/fallback 126 | if cap == Qt.SquareCap: 127 | return mapnik.line_cap.SQUARE_CAP 128 | if cap == Qt.FlatCap: 129 | return mapnik.line_cap.BUTT_CAP 130 | else: 131 | return mapnik.line_cap.ROUND_CAP 132 | 133 | def get_join(join): 134 | """ Turn a Qt Line Join enum into a Mapnik one. 135 | """ 136 | # TODO - convert into a dictionary dispatch 137 | # with proper default/fallback 138 | if join == Qt.BevelJoin: 139 | return mapnik.line_join.BEVEL_JOIN 140 | elif join == Qt.RoundJoin: 141 | return mapnik.line_join.ROUND_JOIN 142 | else: 143 | return mapnik.line_join.MITER_JOIN 144 | # not sure what this one does... 145 | #return mapnik.line_join.MITER_REVERT_JOIN 146 | 147 | def extent_string(e): 148 | """ Return a bbox string from a QGIS extent rectangle.""" 149 | return str('%s,%s,%s,%s' % (e.xMinimum(),e.yMinimum(),e.xMaximum(),e.yMaximum())) 150 | 151 | def extent_tuple(e): 152 | """ Return a bbox tuple from a QGIS extent rectangle.""" 153 | return (e.xMinimum(),e.yMinimum(),e.xMaximum(),e.yMaximum()) 154 | 155 | def unique_filter(attr, low, idx, filter_type): 156 | """ Return a Mapnik filter expression string based on a single value. 157 | """ 158 | d = {'attr':attr,'low':low} 159 | if low and filter_type == QVariant.String: 160 | expr = "[%(attr)s] = '%(low)s'" % d 161 | # TODO support more Qtype checking... 162 | elif is_number(str(low)) or low == 0: 163 | expr = "[%(attr)s] = %(low)s" % d 164 | elif not low and not low == 0: 165 | expr = "not [%(attr)s] <> ''" % d 166 | else: 167 | raise TypeError('unknown field value type: type=%s,val=%s,low=%s,filter_type=%s' % (type(attr),attr,low,filter_type)) 168 | return unicode(expr) 169 | 170 | 171 | def graduated_filter(attr, low, upp, idx, filter_type): 172 | """ Return a Mapnik filter expression string based on an upper and lower value pair. 173 | """ 174 | d = {'attr':attr,'upp':upp,'low':low} 175 | if idx == 0: 176 | d['threshold'] = '>=' 177 | else: 178 | d['threshold'] = '>' 179 | if low and upp: 180 | expr = "[%(attr)s] %(threshold)s %(low)s and [%(attr)s] <= %(upp)s" % d 181 | elif low: 182 | expr = "[%(attr)s] %(threshold)s %(low)s" % d 183 | elif upp: 184 | expr = "[%(attr)s] <= %(upp)s" % d 185 | return unicode(expr) 186 | 187 | def to_wld(mapnik_map, x_rotation=0.0, y_rotation=0.0): 188 | """ Generate a World File string from a mapnik::Map.""" 189 | extent = mapnik_map.envelope() 190 | pixel_x_size = (extent.maxx - extent.minx)/mapnik_map.width 191 | pixel_y_size = (extent.maxy - extent.miny)/mapnik_map.height 192 | upper_left_x_center = extent.minx + 0.5 * pixel_x_size + 0.5 * x_rotation 193 | upper_left_y_center = extent.maxy + 0.5 * (pixel_y_size*-1) + 0.5 * y_rotation 194 | wld_string = '''%.10f\n%.10f\n%.10f\n-%.10f\n%.10f\n%.10f\n''' % ( 195 | pixel_x_size, 196 | y_rotation, 197 | x_rotation, 198 | pixel_y_size, 199 | upper_left_x_center, 200 | upper_left_y_center) 201 | return wld_string 202 | 203 | class RasterRules(object): 204 | """ Class to contruct a set of Rules for QGIS Rasters. 205 | 206 | TODO: this is very preliminary and work needs to be done to: 207 | 208 | * try to sync colors closer 209 | * track down the cause of slight shifts between Mapnik and QGIS 210 | 211 | """ 212 | def __init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor): 213 | self.layer = layer 214 | self.opacity = opacity 215 | self.scale_factor = scale_factor 216 | self.base_path = base_path 217 | self.raster_scale_factor = raster_scale_factor 218 | 219 | @property 220 | def raster_type(self): 221 | # TODO - need to investigate more QGIS types 222 | rt = self.layer.rasterType() 223 | if rt == QgsRasterLayer.Palette: 224 | return 'palette' 225 | elif rt == QgsRasterLayer.Multiband: 226 | return 'multiband' 227 | else: #GrayOrUndefined 228 | return 'undefined' 229 | 230 | def set(self,min_scale=None,max_scale=None): 231 | r_list = [] 232 | r = mapnik.Rule() 233 | if min_scale: 234 | r.min_scale = min_scale 235 | if max_scale: 236 | r.max_scale = max_scale 237 | raster = mapnik.RasterSymbolizer() 238 | raster.opacity = self.opacity 239 | #raster.scaling = 'bilinear' 240 | #raster.mode = 'normal' 241 | r.symbols.append(raster) 242 | r_list.append(r) 243 | return r_list 244 | 245 | class BaseVectorRules(object): 246 | def __init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor,background): 247 | self.layer = layer 248 | self.opacity = opacity 249 | self.scale_factor = scale_factor 250 | self.base_path = base_path 251 | self.raster_scale_factor = raster_scale_factor 252 | self.background = background 253 | if hasattr(layer,'isUsingRendererV2') and layer.isUsingRendererV2(): 254 | self.v2 = True 255 | else: 256 | self.v2 = False 257 | 258 | @property 259 | def point(self): 260 | return self.layer.geometryType() == QGis.Point 261 | 262 | @property 263 | def line(self): 264 | return self.layer.geometryType() == QGis.Line 265 | 266 | @property 267 | def polygon(self): 268 | return self.layer.geometryType() == QGis.Polygon 269 | 270 | @property 271 | def fields(self): 272 | return self.layer.dataProvider().fields() 273 | 274 | def features(self): 275 | # http://doc.qgis.org/head/qgsvectorlayer_8cpp-source.html 276 | if self.v2: 277 | # http://trac.osgeo.org/qgis/changeset/12347 278 | # http://trac.osgeo.org/qgis/changeset/12357 279 | return None # QgsFeatureRendererV2::fieldNameIndex not exposed? 280 | idx = self.layer.renderer().classificationAttributes()[0] 281 | return self.layer.dataProvider().uniqueValues(idx) 282 | 283 | 284 | # todo, merge filter_type and filter_name 285 | @property 286 | def filter_type(self): 287 | if self.v2: 288 | attr = self.layer.rendererV2.usedAttributes()[0] 289 | return type(get_variant_value(attr)) 290 | attr = self.layer.renderer().classificationAttributes() 291 | field = self.fields[attr[-1]] 292 | return field.type() 293 | 294 | def filter_name(self,symbol): 295 | if self.layer.renderer().needsAttributes(): 296 | attr = self.layer.renderer().classificationAttributes() 297 | 298 | # todo 299 | # if scale or rotation are not -1 then len(attr) > 1 300 | #scale_attr = symbol.scaleClassificationField() 301 | 302 | # if multiple, then the one that is used to filter seems 303 | # to be last.... vs the attr used to rotate,scale,etc 304 | # but we likely need to pass symbol to figure this out 305 | fld = self.fields[attr[-1]] 306 | return unicode(fld.name()) 307 | 308 | def get_filter(self,sym,idx): 309 | filter_attr = self.filter_name(sym) 310 | filter_type = self.filter_type 311 | if self.symbolization in ("Unique Value","singleSymbol"): 312 | expr = unique_filter(filter_attr,unicode(sym.lowerValue()),idx,filter_type) 313 | elif self.symbolization in ("Graduated Symbol","graduatedSymbol","categorizedSymbol"): 314 | expr = graduated_filter(filter_attr,unicode(sym.lowerValue()),unicode(sym.upperValue()),idx,filter_type) 315 | # TODO - we can't let unicode or other errors throw exceptions... 316 | if MAPNIK_VERSION >= 800: 317 | return mapnik.Expression(str(expr)) 318 | else: 319 | return mapnik.Filter(str(expr)) 320 | 321 | @property 322 | def symbolization(self): 323 | if self.v2: 324 | if self.layer.rendererV2: 325 | return self.layer.rendererV2.type() 326 | if self.layer.renderer(): # layers will no geometries can lack a renderer 327 | return self.layer.renderer().name() 328 | 329 | @property 330 | def symbols(self): 331 | # todo - http://trac.osgeo.org/qgis/changeset/12328 332 | return self.layer.renderer().symbols() 333 | 334 | class VectorLabels(BaseVectorRules): 335 | def __init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor,background): 336 | BaseVectorRules.__init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor,background) 337 | 338 | @property 339 | def face_name(self): 340 | return "DejaVu Sans Bold" 341 | 342 | @property 343 | def label(self): 344 | return self.layer.label() 345 | 346 | @property 347 | def field_name(self): 348 | return unicode(self.label.labelField(self.label.LabelField())) 349 | 350 | @property 351 | def attr(self): 352 | return self.label.layerAttributes() 353 | 354 | def family(self): 355 | pass 356 | # TODO 357 | # a.family() Lucida Grande 358 | 359 | def set(self,min_scale=None,max_scale=None): 360 | r_list = [] 361 | a = self.attr 362 | r = mapnik.Rule() 363 | if min_scale or max_scale: 364 | if min_scale: 365 | r.min_scale = min_scale 366 | if max_scale: 367 | r.max_scale = max_scale 368 | elif self.label.scaleBasedVisibility(): 369 | # these scales don't map correctly at first glance 370 | # need to look closer at different pixel assumptions... 371 | pixel_factor = (90.1/72) 372 | r.min_scale = self.layer.minimumScale() * pixel_factor 373 | r.max_scale = self.layer.maximumScale() * pixel_factor 374 | # note mapnik labels appear bigger so we reduce the size.... 375 | text_size = a.size() * self.scale_factor * .8 376 | fill = css_color(a.color()) 377 | field_name = str(self.field_name) 378 | if not field_name: 379 | return [] 380 | if MAPNIK_VERSION >= 800: 381 | name = mapnik.Expression("[%s]" % field_name) 382 | else: 383 | name = field_name 384 | text = mapnik.TextSymbolizer(name,self.face_name,int(text_size),fill) 385 | if a.bufferEnabled(): 386 | col = css_color(a.bufferColor()) 387 | # silently fail with mapnik versions pre 0.6.0 388 | # which may not support alpha colors this same way 389 | try: 390 | col.a = int(.3*255) # force slighly transparent halos 391 | except: 392 | pass 393 | text.halo_fill = col 394 | text.halo_radius = int(a.bufferSize() * self.scale_factor) # float 395 | if a.borderWidth() > 0: 396 | text.halo_fill = css_color(a.borderColor()) 397 | if a.multilineEnabled(): 398 | text.wrap_width = 100 # better default? 399 | dx, dy = 0,0 400 | if self.line: 401 | # if the geometry is a line, then lets wrap text along it 402 | text.label_placement = mapnik.label_placement.LINE_PLACEMENT 403 | # put test above line rather than directly on top 404 | dy = text_size * .7 405 | # throw out labels if they are on sharp turns 406 | text.max_char_angle_delta = 20 407 | # make sure not to place duplicate labels to close together 408 | text.label_spacing = 50 409 | text.minimum_distance = 200 410 | elif self.point: 411 | # don't let points overlap with other stuff 412 | text.allow_overlap = False 413 | # to try to get more placed, displace text off of point geometry 414 | # this will push text to the upper right 415 | # todo - need to bump up more for Cairo text output as Cairo is slightly larger 416 | dx, dy = text_size * .8,text_size * .8 417 | else: 418 | text.allow_overlap = False 419 | 420 | try: 421 | text.displacement(dx,dy) 422 | except: 423 | text.displacement = (dx,dy) 424 | 425 | # defaults... 426 | if dy == 0: 427 | text.vertical_alignment = mapnik.vertical_alignment.MIDDLE 428 | elif dy > 0: 429 | text.vertical_alignment = mapnik.vertical_alignment.BOTTOM 430 | elif dy < 0: 431 | text.vertical_alignment = mapnik.vertical_alignment.TOP 432 | text.avoid_edges = True 433 | 434 | # available in Mapnik >=0.7.0 435 | # only for point placment 436 | #try: 437 | #text.wrap_character = ';' 438 | #text.line_spacing = 20 439 | #text.character_spacing = 20 440 | # applies to both lines and points 441 | #text.text_transform = mapnik.text_transform.UPPERCASE 442 | #except: pass 443 | 444 | r.symbols.append(text) 445 | r_list.append(r) 446 | return r_list 447 | 448 | class VectorRules(BaseVectorRules): 449 | def __init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor,background): 450 | self.idx = 0 451 | BaseVectorRules.__init__(self,layer,opacity,scale_factor,base_path,raster_scale_factor,background) 452 | 453 | def set(self,min_scale=None,max_scale=None): 454 | if self.symbolization is None: 455 | return [] 456 | if self.symbolization == 'Single Symbol': 457 | # here we assume adjacent polygons and up the gamma slighly based on the 458 | # wild guess that this is desirable 459 | gamma = 0.7 460 | return [self.single(self.symbols[0],min_scale=min_scale,max_scale=max_scale,gamma=gamma)] 461 | elif self.symbolization == 'Graduated Symbol': 462 | return self.values(self.symbols,min_scale=min_scale,max_scale=max_scale) 463 | elif self.symbolization == 'Unique Value': 464 | return self.values(self.symbols,min_scale=min_scale,max_scale=max_scale) 465 | elif self.symbolization == 'Continuous Color': 466 | return self.continuous_values(self.symbols,min_scale=min_scale,max_scale=max_scale) 467 | elif self.symbolization == 'OSM': 468 | return self.values(self.symbols,min_scale=min_scale,max_scale=max_scale) 469 | else: 470 | raise Exception("not implemented yet") 471 | 472 | def point_sym(self,symbol,color=None): 473 | # what out for sketchy QImages! 474 | # http://blog.qgis.org/node/74 475 | color = QColor() 476 | width_scale = 1 477 | scale = 1 478 | rotation = 0 479 | allow_overlap = False 480 | filename = None 481 | path_expression = None 482 | #symbol.pointSize() 483 | attr = symbol.scaleClassificationField() 484 | # This nasty code will be replaced once Mapnik has proper 485 | # support for dynamically scaling SVG symbols.... 486 | if attr >= 0 and MAPNIK_VERSION >= 800: 487 | allow_overlap = True 488 | if self.idx == 0: 489 | features = self.layer.dataProvider().uniqueValues(attr) 490 | vals = [] 491 | for item in features: 492 | field_value = get_variant_value(item) 493 | #print '%.25f' % field_value 494 | if item.type() == QVariant.Double: 495 | if int(item.toDouble()[0]) == item.toDouble()[0]: 496 | # check if it is actually an int 497 | mapnik_field_value = int(item.toDouble()[0]) 498 | else: 499 | # okay, its actually a float... 500 | # due dirty things to try to match Mapnik's float precision 501 | m_float = '%.25f' % round(item.toByteArray().toFloat()[0],13) 502 | decimals = m_float.split('.')[1][:13] 503 | mapnik_field_value = '%s.%s' % (m_float.split('.')[0],decimals) 504 | else: 505 | mapnik_field_value = field_value # assuming int 506 | 507 | if not field_value in vals: 508 | vals.append(field_value) 509 | # mapnik uses 13 digits after decimal 510 | # QGIS rounds to 10 when saving image... 511 | filename = 'sym_%s.png' % mapnik_field_value 512 | if self.base_path: 513 | filename = os.path.join(self.base_path,filename) 514 | scale_value = math.sqrt(math.fabs(field_value)) 515 | #if not os.path.exists(filename): 516 | # todo, find a way to match color to filter name 517 | q_im = symbol.getPointSymbolAsImage(width_scale,False,color,scale_value,rotation,self.raster_scale_factor) 518 | q_im.save(filename) 519 | 520 | fld = self.fields[attr] 521 | if self.base_path: 522 | path_expression = os.path.join(self.base_path,'sym_[%s].png' % str(fld.name())) 523 | else: 524 | path_expression = 'sym_[%s].png' % str(fld.name()) 525 | self.idx += 1 526 | else: 527 | try: 528 | q_im = symbol.getPointSymbolAsImage(width_scale,False,color,scale,rotation,self.raster_scale_factor) 529 | except: 530 | # http://doc.qgis.org/stable/qgssymbol_8cpp-source.html#l00315 531 | q_im = symbol.getPointSymbolAsImage(width_scale,False,color) 532 | 533 | if self.base_path: 534 | filename = os.path.join(self.base_path,'sym_%s.png' % self.idx) 535 | else: 536 | filename = 'sym_%s.png' % self.idx 537 | self.idx += 1 538 | q_im.save(filename) 539 | 540 | #q_im.hasAlphaChannel() 541 | if MAPNIK_VERSION >= 800: 542 | point = mapnik.PointSymbolizer(mapnik.PathExpression(path_expression or filename)) 543 | else: 544 | w,h = q_im.width(),q_im.height() 545 | point = mapnik.PointSymbolizer(filename,'png',w,h) 546 | point.opacity = self.opacity 547 | point.allow_overlap = allow_overlap 548 | return point 549 | 550 | def line_sym(self,symbol,color=None,m2q_factor=.7,dashes=True): 551 | # make mapnik line width thicker 552 | w = symbol.lineWidth() + m2q_factor 553 | p = symbol.pen() 554 | stroke = mapnik.Stroke() 555 | stroke.width = w * self.scale_factor 556 | if color: 557 | stroke.color = color 558 | else: 559 | stroke.color = css_color(symbol.color()) 560 | stroke.opacity = self.opacity 561 | # make mapnik dash spacing longer 562 | dash_array = [i + m2q_factor for i in p.dashPattern()] 563 | if dashes and dash_array: 564 | stroke.add_dash(*dash_array[:2]) 565 | if len (dash_array) > 2: 566 | stroke.add_dash(*dash_array[2:4]) 567 | if len (dash_array) > 4: 568 | stroke.add_dash(*dash_array[4:6]) 569 | # to reduce verbosity in XML output only respect 570 | # user choices or QT defaults (more likely) 571 | # for endstyles with linear geometries 572 | # as Mapnik defaults are different than QT 573 | if self.line: 574 | stroke.line_join = get_join(p.joinStyle()) 575 | stroke.line_cap = get_cap(p.capStyle()) 576 | line = mapnik.LineSymbolizer(stroke) 577 | return line 578 | 579 | def polygon_pattern_sym(self,symbol): 580 | file_ = str(symbol.customTexture()) 581 | im = QImage(symbol.customTexture()) 582 | if MAPNIK_VERSION >= 800: 583 | poly_pattern = mapnik.PolygonPatternSymbolizer(mapnik.PathExpression(file_)) 584 | else: 585 | poly_pattern = mapnik.PolygonPatternSymbolizer(file_,'png',im.width(),im.height()) 586 | return poly_pattern 587 | 588 | def polygon_sym(self,symbol,color=None,gamma=None): 589 | if color: 590 | poly = mapnik.PolygonSymbolizer(color) 591 | else: 592 | poly = mapnik.PolygonSymbolizer(css_color(symbol.fillColor())) 593 | poly.fill_opacity = self.opacity 594 | if gamma: 595 | poly.gamma = gamma 596 | return poly 597 | 598 | def continuous_values(self,syms,min_scale=None,max_scale=None): 599 | r_list = [] 600 | low = syms[0].lowerValue() 601 | high = syms[1].lowerValue() 602 | #high = syms[1].upperValue() 603 | s = syms[0] 604 | s.setLowerValue('%s' % low) 605 | s.setUpperValue('%s' % high) 606 | # should we be using fillColor() ?? 607 | start = css_color(syms[1].color()) 608 | end = css_color(syms[0].color()) 609 | 610 | ramp = Ramp(start,end,float(low),float(high)) 611 | filter_type = self.filter_type 612 | features = self.features() 613 | for idx, feat in enumerate(features): 614 | val = get_variant_value(feat) 615 | color_tuple = ramp.color_for_value(float(val)) 616 | color = mapnik.Color('rgb(%s,%s,%s)' % color_tuple[:3]) 617 | r = self.single(s,color=color,outline=False,min_scale=min_scale,max_scale=max_scale) 618 | filter_attr = self.filter_name(s) 619 | filt = unique_filter(filter_attr,unicode(val),idx,filter_type) 620 | if filt: 621 | r.filter = mapnik.Filter(str(filt)) 622 | r_list.append(r) 623 | 624 | # currently not exposed in python... 625 | # need to check for renderer->drawPolygonOutline() 626 | # default to no outlines... 627 | #if self.polygon: 628 | # r = mapnik.Rule() 629 | # r.symbols.append(self.line_sym(s,mapnik.Color('black'))) 630 | # r_list.append(r) 631 | return r_list 632 | 633 | def values(self,syms,min_scale=None,max_scale=None): 634 | r_list = [] 635 | for idx, s in enumerate(syms): 636 | r = self.single(s,min_scale=min_scale,max_scale=max_scale,gamma=.65) 637 | filt = self.get_filter(s,idx) 638 | if filt: 639 | r.filter = filt 640 | r_list.append(r) 641 | if not len(syms): 642 | r_list.append(self.default()) 643 | return r_list 644 | 645 | def default(self): 646 | r = mapnik.Rule() 647 | if self.point: 648 | r.symbols.append(mapnik.PointSymbolizer()) 649 | elif self.line: 650 | r.symbols.append(mapnik.LineSymbolizer()) 651 | elif self.polygon: 652 | r.symbols.append(mapnik.PolygonSymbolizer()) 653 | return r 654 | 655 | def single(self,sym,color=None,outline=True,min_scale=None,max_scale=None,gamma=None): 656 | r = mapnik.Rule() 657 | if min_scale: 658 | r.min_scale = min_scale 659 | if max_scale: 660 | r.max_scale = max_scale 661 | if self.point: 662 | if not sym.pen().style() == Qt.NoPen or not sym.brush().style() == Qt.NoBrush: 663 | r.symbols.append(self.point_sym(sym,color)) 664 | elif self.line: 665 | if not sym.pen().style() == Qt.NoPen: 666 | r.symbols.append(self.line_sym(sym,color)) 667 | if self.polygon: 668 | if not sym.brush().style() == Qt.NoBrush: 669 | # hmm qgis 1.60 reports customTexture() when it should not 670 | # TODO - how to detect a texture rather than solid fill? 671 | #if sym.customTexture(): 672 | # r.symbols.append(self.polygon_pattern_sym(sym)) 673 | #else: 674 | r.symbols.append(self.polygon_sym(sym,color,gamma=gamma)) 675 | #if sym.pen().style() == Qt.NoPen: 676 | # # no outlines, so apply gamm fix if gamma is set 677 | # # which it will be if we are using single symbolization 678 | # r.symbols.append(self.polygon_sym(sym,color,gamma=gamma)) 679 | #else: 680 | # # ignore the gamma because we have outlines 681 | # r.symbols.append(self.polygon_sym(sym,color,gamma=None)) 682 | if not sym.pen().style() == Qt.NoPen and outline: 683 | primary = self.line_sym(sym,color) 684 | if len(sym.pen().dashPattern()): 685 | # TODO - expose option to alternate the inverse of the line color 686 | # as the background as that can look sharp 687 | # for now we'll use the background color if not filling polygon 688 | # and the polygon color if we are filling 689 | if sym.brush().style() == Qt.NoBrush: 690 | color = self.background 691 | else: 692 | color = css_color(sym.fillColor()) 693 | underneath_line = self.line_sym(sym,color=color,dashes=False) 694 | r.symbols.append(underneath_line) 695 | r.symbols.append(primary) 696 | # todo - need to avoid attaching an empty rule. 697 | return r 698 | 699 | class LayerAdaptor(object): 700 | def __init__(self,parent,layer,scale_factor,base_path,raster_scale_factor,background): 701 | self.parent = parent 702 | self.layer = layer 703 | self.scale_factor = scale_factor 704 | self.base_path = base_path 705 | self.raster_scale_factor = raster_scale_factor 706 | self.background = background 707 | self.vector_lyr = (layer.type() == layer.VectorLayer) 708 | self.raster_lyr = (layer.type() == layer.RasterLayer) 709 | self._datasource = None 710 | 711 | def message(self,msg): 712 | QMessageBox.information(self.parent.iface.mainWindow(),"Warning",QString(msg)) 713 | return 714 | 715 | @property 716 | def opacity(self): 717 | return self.layer.getTransparency()/255.0 718 | 719 | @property 720 | def extent(self): 721 | # not predicatable... 722 | #return self.layer.dataProvider().extent() 723 | return self.layer.extent() 724 | 725 | def uri(self): 726 | return QgsDataSourceURI(self.layer.dataProvider().dataSourceUri()) 727 | 728 | @property 729 | def provider(self): 730 | return str(self.layer.dataProvider().name()) 731 | #return str(self.layer.providerType()) 732 | 733 | def datasource(self): 734 | global INCOMPATIBLE_LAYER_WARNING 735 | if self.raster_lyr: 736 | if self.is_geotiff and 'gdal' not in MAPNIK_PLUGINS: 737 | if self.layer.width() > 10000 or self.layer.height() > 10000: 738 | # gdal is not available, but the user should install it 739 | # rather than trying to read such a large file with the 740 | # raster datasource without overviews support... 741 | return self.gdal() 742 | return self.raster() 743 | if '.sqlite' in self.source: 744 | if 'rasterlite' in MAPNIK_PLUGINS: 745 | # nope, we have no way to get the table name... 746 | #return self.rasterlite() 747 | pass 748 | if not self.layer.providerType() == 'gdal': 749 | # grass rasters and some tif's do not have a provider method! 750 | # others? 751 | # TODO - needs testing 752 | return self.gdal() 753 | elif self.provider == 'grass': 754 | # TODO - needs testing 755 | return self.gdal() 756 | # disable WMS support since it is 757 | # not working well yet... 758 | elif self.provider == 'wms': 759 | pass 760 | # return self.wms() 761 | else: 762 | return self.gdal() 763 | elif self.vector_lyr: 764 | # grass: http://bitbucket.org/springmeyer/quantumnik/issue/14/ 765 | if self.provider == 'postgres': 766 | return self.postgis() 767 | elif self.provider == 'grass': 768 | return self.grass_vector() 769 | elif self.provider == 'ogr': 770 | if self.is_shape and not os.path.isdir(self.source): 771 | # re: the isdir() call.. zipped shapefiles when 772 | # extracted are often dumped into 'folder.shp' 773 | # so instead of trying to reach inside, lets 774 | # just let ogr read the files, which it can 775 | # from within an arbitrary directory 776 | return self.shape() 777 | return self.ogr() 778 | elif self.provider == 'spatialite': 779 | return self.sqlite() 780 | elif self.provider == 'osm': 781 | return self.osm() 782 | # kludgy - will get hit if any one kind of datasource is unsupported 783 | # goal is to support most all datasources, so hopefully we can remove this soon 784 | # with format specific errors where there exist incompatibilities 785 | if INCOMPATIBLE_LAYER_WARNING: 786 | self.message('You will see this warning once per session... Quantumnik does not currently support "%s" datasources. You will need to uncheck any unsupported layers before rendering with Quantumnik otherwise the resulting map will be blank. File an issue at https://github.com/springmeyer/quantumnik if you would like to request support for this format...' % self.provider) 787 | INCOMPATIBLE_LAYER_WARNING = False 788 | 789 | @property 790 | def sub_layer_name(self): 791 | # hack! QGIS needs to provide ogr sublayers as easy 792 | # to fetch attributes 793 | sub_layers = self.layer.dataProvider().subLayers() 794 | if sub_layers: 795 | # TODO: predictably handle more than one sublayer 796 | 797 | # can't recall for what layer type this 798 | # code below actually worked for! 799 | #for sl in sub_layers: 800 | # if str(self.layer.name()) in sl: 801 | # return str(self.layer.name()) 802 | 803 | layer_string = None 804 | # support gpx data 805 | for sl in sub_layers: 806 | if str(self.layer.name()).lower() in sl: 807 | layer_string = str(sl) 808 | break 809 | 810 | # TODO: handle more than one sublayer 811 | if not layer_string: 812 | layer_string = str(sub_layers[0]) 813 | # wms sublayers appear to be just the 814 | # layer name - this is trouble! 815 | if self.provider == 'wms': 816 | return layer_string 817 | # other layers via ogr look like: 818 | # LayerIndex : LayerName : FeatureCount : GeometryType 819 | # 0:ARC:30849:LineString 820 | pattern = r'\d+:(.+):\d+:' 821 | return re.findall(pattern, layer_string)[0] 822 | else: 823 | # likely will not match ogr name 824 | # but worth the try... 825 | return str(self.layer.name()) 826 | 827 | # TODO - avoid duplicate style/layer names... 828 | #@property 829 | #def unique_name(self): 830 | # # ugly, but a reliably unique name 831 | # return str(self.layer.getLayerID()) 832 | 833 | def name(self): 834 | # this is the 'display name' that can be be set in the layers general options 835 | name = str(self.layer.name()) 836 | # shapefiles return the full path which is too verbose 837 | if os.path.sep in name: 838 | name = os.path.basename(name) 839 | try: 840 | return str(os.path.splitext(name)[0]) 841 | except: 842 | return str(name) 843 | 844 | @property 845 | def has_labels(self): 846 | if self.vector_lyr: 847 | return self.layer.hasLabelsEnabled() 848 | return False 849 | 850 | def pk_field_name(self): 851 | return self.layer.dataProvider().fields()[0].name() 852 | 853 | #@check_plug 854 | def rasterlite(self): 855 | pass 856 | 857 | #@check_plug 858 | def osm(self): 859 | #/Users/spring/projects/haiti/latest.osm?type=point&tag=name&style=/Applications/Qgis.app/Contents/Resources/python/plugins/osm/styles/small_scale.style 860 | osm_file = str(self.source.split('?')[0]) 861 | sqlite_osm_db = '%s.db' % osm_file 862 | if os.path.exists(sqlite_osm_db): 863 | # >>> print r.classificationAttributes() 864 | #[2] 865 | params = {} 866 | params['file'] = sqlite_osm_db 867 | if 'polygon' in str(self.source): 868 | params['table'] = '(Select * from way where closed = 1) as t' 869 | elif 'line' in str(self.source): 870 | params['table'] = '(Select * from way where closed = 0) as t' 871 | elif 'point' in str(self.source): 872 | # need to turn lat,lon into geometry... 873 | params['table'] = 'way' 874 | params['geometry_field'] = 'wkb' 875 | #params['wkb_format'] = str('spatialite') 876 | #params['use_spatial_index'] = True 877 | params['key_field'] = 'i' 878 | params['extent'] = extent_string(self.extent) 879 | return mapnik.SQLite(**params) 880 | return mapnik.Osm(file=osm_file) 881 | 882 | @check_plug 883 | def shape(self): 884 | # bug in mapnik prevents multipoint reading using shape plugin 885 | # so, we'll use ogr plugin instead for < Mapnik 0.7.0 and request 886 | # exploded geoms to workaround a bug in ogr driver 887 | # http://trac.mapnik.org/ticket/458 888 | # http://doc.qgis.org/head/classQGis.html#8da456870e1caec209d8ba7502cceff7 889 | if MAPNIK_VERSION: 890 | if self.layer.wkbType() == QGis.WKBPoint25D: 891 | # http://trac.mapnik.org/ticket/504 892 | if not MAPNIK_VERSION >= 800: 893 | return self.ogr() 894 | if self.layer.wkbType() == QGis.WKBMultiPoint: 895 | if MAPNIK_VERSION >= 700: 896 | return mapnik.Shapefile(file=self.source) 897 | else: 898 | return self.ogr(multiple_geometries=True) 899 | if MAPNIK_VERSION > 600: 900 | # Mapnik 0.6.0 and greater supports creating 901 | # shapefile datasources using the '.shp' extension 902 | return mapnik.Shapefile(file=self.source) 903 | elif self.layer.wkbType() == QGis.WKBMultiPoint: 904 | return self.ogr(multiple_geometries=True) 905 | return mapnik.Shapefile(file=self.source.replace('.shp','')) 906 | 907 | @check_plug 908 | def ogr(self,multiple_geometries=False): 909 | return mapnik.Ogr(file=self.source,layer=self.sub_layer_name,multiple_geometries=multiple_geometries) 910 | 911 | def wms(self): 912 | params = {} 913 | params['format'] = 'image/png' 914 | params['layers'] = self.sub_layer_name 915 | params['styles'] = '' 916 | # currently can't get at actual version being requested... 917 | params['version'] = '1.1.1' 918 | params['url'] = self.source + '?' 919 | params['srs'] = self.authid() 920 | params['projection'] = self.authid() 921 | e = self.layer.extent() 922 | params['ulx'] = e.xMinimum() # flipped 923 | params['uly'] = e.yMaximum() 924 | params['llx'] = e.xMaximum() # flipped 925 | params['lly'] = e.yMinimum() 926 | wms_template = ''' 927 | 928 | %(version)s 929 | %(url)s 930 | 931 | 932 | %(layers)s 933 | %(styles)s 934 | 935 | 936 | %(ulx)s 937 | %(uly)s 938 | %(llx)s 939 | %(lly)s 940 | 2949120 941 | 1474560 942 | 943 | %(projection)s 944 | 12 945 | 256 946 | 256 947 | 3 948 | 949 | ''' % params 950 | print wms_template 951 | (handle, service_description) = tempfile.mkstemp('.xml', 'qnik_gdal_wms-') 952 | os.close(handle) 953 | open(service_description, 'w').write(wms_template) 954 | return mapnik.Gdal(file=service_description) 955 | 956 | @check_plug 957 | def postgis(self): 958 | params = {} 959 | # a bunch of stuff exposed in 1.1 960 | # http://trac.osgeo.org/qgis/changeset/10581 961 | uri = self.uri() 962 | # dbname='aussie' user='postgres' table="osm_au_polygon" (way) sql= 963 | # uri = QgsDataSourceURI(iface.mapCanvas().layer(0).dataProvider().dataSourceUri()) 964 | # quote potentially changed in http://trac.osgeo.org/qgis/changeset/13336 965 | params['extent'] = extent_string(self.extent) 966 | params['estimate_extent'] = False 967 | params['user'] = str(uri.username()) 968 | if uri.schema(): 969 | table = '"%s"."%s"' % (str(uri.schema()),str(uri.table())) 970 | else: 971 | table = '"%s"' % str(uri.table()) 972 | 973 | if uri.sql(): 974 | where = str(uri.sql()) 975 | params['table'] = '(SELECT * FROM %s WHERE %s) as %s' % (table,where,'"%s"' % str(uri.table())) 976 | else: 977 | params['table'] = table 978 | 979 | if hasattr(uri, 'database'): 980 | params['dbname'] = str(uri.database()) 981 | else: 982 | params['dbname'] = str(uri.connectionInfo().split(' ')[0].split('=')[1]) 983 | if hasattr(uri, 'database'): 984 | if uri.password(): 985 | params['password'] = str(uri.password()) 986 | if hasattr(uri, 'host'): 987 | if uri.host(): 988 | params['host'] = str(uri.host()) 989 | if hasattr(uri, 'port'): 990 | if uri.port(): 991 | params['port'] = str(uri.port()) 992 | params['geometry_field'] = str(uri.geometryColumn()) 993 | if hasattr(self.layer,'crs'): 994 | params['srid'] = self.layer.crs().postgisSrid() 995 | else: 996 | params['srid'] = self.layer.srs().postgisSrid() # deprecated 997 | return mapnik.PostGIS(**params) 998 | 999 | @check_plug 1000 | def sqlite(self): 1001 | params = {} 1002 | uri = self.uri() 1003 | params['file'] = str(uri.database()) 1004 | params['table'] = str(uri.table()) 1005 | params['geometry_field'] = str(uri.geometryColumn()) 1006 | params['wkb_format'] = str('spatialite') 1007 | params['use_spatial_index'] = True 1008 | # this assumption is likely going to fall apart! 1009 | # how can we predictably get the PK name? 1010 | params['key_field'] = str(self.pk_field_name()) 1011 | params['extent'] = extent_string(self.extent) 1012 | return mapnik.SQLite(**params) 1013 | 1014 | @check_plug 1015 | def gdal(self): 1016 | # todo - check for GDAL version and determine whether 1017 | # to used shared opening option 1018 | #return mapnik.Gdal(file=self.source,shared=True) 1019 | return mapnik.Gdal(file=self.source) 1020 | 1021 | def grass_vector(self): 1022 | # we are going to connect through OGR... 1023 | # since the grass provider does not provide 1024 | # a descent connection string to pass to ogr 1025 | # this is going to be ugly and error prone 1026 | # http://gdal.org/ogr/drv_grass.html 1027 | src = os.path.normpath(self.source) 1028 | parts = src.split(os.path.sep) 1029 | grass_base = os.path.sep.join(parts[:len(parts)-2]) 1030 | dataset = parts[-2:-1][0] 1031 | v_file = os.path.join(grass_base,'vector',dataset,'head') 1032 | layer = parts[-1:][0].split('_')[0] 1033 | return mapnik.Ogr(file=v_file,layer=layer) 1034 | 1035 | @check_plug 1036 | def raster(self): 1037 | d= {} 1038 | d['file'] = self.source 1039 | lox,loy,hix,hiy = extent_tuple(self.extent) 1040 | d['lox'] = lox 1041 | d['loy'] = loy 1042 | d['hix'] = hix 1043 | d['hiy'] = hiy 1044 | return mapnik.Raster(**d) 1045 | 1046 | @property 1047 | def source(self): 1048 | src = str(self.layer.source()) 1049 | # hack to support e00 files that 1050 | # seem to tack on the layername after 1051 | # a pipe - others do perhaps as well? 1052 | if '|' in src: 1053 | return src.split('|')[0] 1054 | return src 1055 | 1056 | @property 1057 | def is_shape(self): 1058 | return self.source.endswith('shp') 1059 | 1060 | @property 1061 | def is_geotiff(self): 1062 | # TODO need to check for strips or tiles... 1063 | return self.source.endswith('tiff') or self.source.endswith('tif') 1064 | 1065 | @property 1066 | def srs(self): 1067 | srid = self.authid() 1068 | if not srid: 1069 | if hasattr(self.layer,'crs'): 1070 | return str(self.layer.crs().toProj4()) 1071 | return str(self.layer.srs().toProj4()) # deprecated 1072 | try: 1073 | # if the proj4 library that Mapnik 1074 | # is linked against knows about this 1075 | # projection by epsg code then lets 1076 | # use that to assign the srs to layers 1077 | return mapnik.Projection('+init=%s' % srid).params() 1078 | except: 1079 | try: 1080 | proj_init = '+init=epsg:%s' % self.layer.srs().epsg() #deprecated 1081 | return mapnik.Projection(proj_init).params() 1082 | except: 1083 | # otherwise initialize with the proj literal 1084 | if hasattr(self.layer,'crs'): 1085 | return str(self.layer.crs().toProj4()) 1086 | return str(self.layer.srs().toProj4()) 1087 | 1088 | def authid(self): 1089 | if hasattr(self.layer,'crs'): 1090 | self.layer.srs = self.layer.crs 1091 | 1092 | if hasattr(self.layer.srs(),'authid'): 1093 | return str(self.layer.srs().authid()) 1094 | # deprecated 1095 | return str('EPSG:%s' % self.layer.srs().epsg()) 1096 | 1097 | def get_style(self,min_scale=None,max_scale=None): 1098 | # todo - detect pen/brush = false 1099 | # e.g... 1100 | # if not self.has_symbology: 1101 | # return None 1102 | 1103 | style = mapnik.Style() 1104 | if self.vector_lyr: 1105 | rules = VectorRules(self.layer,self.opacity,self.scale_factor,self.base_path,self.raster_scale_factor,self.background) 1106 | style.rules.extend(rules.set(min_scale=min_scale,max_scale=max_scale)) 1107 | elif self.raster_lyr: 1108 | rules = RasterRules(self.layer,self.opacity,self.scale_factor,self.base_path,self.raster_scale_factor) 1109 | style.rules.extend(rules.set(min_scale=min_scale,max_scale=max_scale)) 1110 | else: 1111 | raise Exception('Type not yet supported') 1112 | return style 1113 | 1114 | def get_label_style(self): 1115 | style = mapnik.Style() 1116 | rules = VectorLabels(self.layer,self.opacity,self.scale_factor,self.base_path,self.raster_scale_factor,self.background) 1117 | style.rules.extend(rules.set()) 1118 | return style 1119 | 1120 | def is_valid(self): 1121 | ds = self.datasource() 1122 | if ds: 1123 | self._datasource = ds 1124 | return True 1125 | return False 1126 | 1127 | def get_min_max_scales(self): 1128 | pixel_factor = (90.1/72) 1129 | min_ = self.layer.minimumScale() * pixel_factor 1130 | max_ = self.layer.maximumScale() * pixel_factor 1131 | return min_,max_ 1132 | 1133 | @property 1134 | def scale_based(self): 1135 | return self.layer.hasScaleBasedVisibility() 1136 | 1137 | def to_mapnik(self,name=None,scale_based=True): 1138 | if not name: 1139 | name = self.name() 1140 | lyr = mapnik.Layer(name,self.srs) 1141 | if self._datasource: 1142 | lyr.datasource = self._datasource 1143 | else: 1144 | lyr.datasource = self.datasource() 1145 | if scale_based and self.scale_based: 1146 | lyr.minzoom,lyr.maxzoom = self.get_min_max_scales() 1147 | #lyr.queryable = True 1148 | #lyr.clear_label_cache = True 1149 | #lyr.active 1150 | #lyr.abstract 1151 | #lyr.title 1152 | # add extra attributes 1153 | #lyr.style = self.style 1154 | #if self.vector_lyr: 1155 | #lyr.label_style = self.get_label_style 1156 | #else: 1157 | # lyr.label_style = None 1158 | return lyr 1159 | 1160 | class EasyCanvas(object): 1161 | def __init__(self,parent,canvas,resolution=90.714): 1162 | self.parent = parent 1163 | self.canvas = canvas 1164 | self.resolution = resolution 1165 | self.width = canvas.width() 1166 | self.height = canvas.height() 1167 | self.normal_pixel = 90.714 1168 | self.base_path = tempfile.gettempdir() 1169 | # TODO - expose as user options... 1170 | self.merge_duplicate_layers = True 1171 | 1172 | def message(self,msg): 1173 | QMessageBox.information(self.parent.iface.mainWindow(),"Warning",QString(msg)) 1174 | return 1175 | 1176 | def raster_scale_factor(self): 1177 | if hasattr(self.canvas.mapRenderer(),'rendererContext'): 1178 | return self.canvas.mapRenderer().rendererContext().scaleFactor() 1179 | else: ## QGIS < 1.2 1180 | return 2.2 1181 | 1182 | @property 1183 | def scale_factor(self): 1184 | return self.resolution/self.normal_pixel 1185 | 1186 | @property 1187 | def dimensions(self): 1188 | w = self.width * self.scale_factor 1189 | h = self.height * self.scale_factor 1190 | return (int(w),int(h)) 1191 | 1192 | @property 1193 | def background(self): 1194 | return css_color(self.canvas.backgroundBrush().color()) 1195 | 1196 | @property 1197 | def srs(self): 1198 | ren = self.canvas.mapRenderer() 1199 | srs_obj = None 1200 | if not ren.hasCrsTransformEnabled(): 1201 | # if we are not projecting on the fly... 1202 | if self.canvas.layerCount() == 1: 1203 | # check if we only have one layer and if so 1204 | # we make the map projection the same as this 1205 | # layers projection because in many circumstances 1206 | # QGIS will actually report the default WGS84 srs 1207 | # when QGIS is actually ignoring its presence because 1208 | # it is not reprojecting on the fly... 1209 | if hasattr(self.canvas.layer(0),'crs'): 1210 | srs_obj = self.canvas.layer(0).crs() 1211 | else: 1212 | srs_obj = self.canvas.layer(0).srs() 1213 | elif self.canvas.layerCount() > 1: 1214 | # otherwise do a clumsy check to see if all layers 1215 | # are actually in the same projection and if so 1216 | # then use the projection of the first layer 1217 | first = self.canvas.layer(0) 1218 | if hasattr(first,'crs'): 1219 | first.srs = first.crs 1220 | if not False in [first.srs() == self.canvas.layer(i).srs() for i in xrange(self.canvas.layerCount())]: 1221 | if hasattr(first,'crs'): 1222 | srs_obj = self.canvas.layer(0).crs() 1223 | else: 1224 | srs_obj = self.canvas.layer(0).srs() 1225 | # otherwise we are reprojecting on the fly and we'll set 1226 | # the map projection to what QGIS actually reports 1227 | if not srs_obj: 1228 | if hasattr(self.canvas.mapRenderer(),'destinationCrs'): 1229 | srs_obj = self.canvas.mapRenderer().destinationCrs() 1230 | else: 1231 | srs_obj = self.canvas.mapRenderer().destinationSrs() 1232 | 1233 | 1234 | srid = '' 1235 | if hasattr(srs_obj,'authid'): 1236 | srid = str(srs_obj.authid()) 1237 | 1238 | try: 1239 | # if the proj4 library that Mapnik 1240 | # is linked against knows about this 1241 | # projection by epsg code then lets 1242 | # use that to assign the srs to layers 1243 | return mapnik.Projection('+init=%s' % srid).params() 1244 | except: 1245 | try: 1246 | proj_init = '+init=epsg:%s' % srs_obj.epsg() #deprecated 1247 | return mapnik.Projection(proj_init).params() 1248 | except: 1249 | # otherwise initialize with the proj literal 1250 | if hasattr(srs_obj,'crs'): 1251 | return str(srs_obj.toProj4()) 1252 | return str(srs_obj.toProj4()) 1253 | 1254 | def to_mapnik(self,m=None): 1255 | global INCOMPATIBLE_RENDERER_WARNING 1256 | 1257 | if m: 1258 | m.remove_all() 1259 | else: 1260 | m = mapnik.Map(*self.dimensions) 1261 | m.srs = self.srs 1262 | #m.background = mapnik.Color('transparent') 1263 | # now that we are drawing on our own mapnik 1264 | # qCanvas, we should respect the background 1265 | # color of the main canvas.. 1266 | m.background = self.background 1267 | 1268 | layer_count = self.canvas.layerCount() 1269 | 1270 | # if not layers, return an empty map 1271 | if not layer_count: 1272 | return m 1273 | 1274 | # switch the layer order 1275 | layer_list = range(layer_count) 1276 | layer_list.reverse() 1277 | #### TODO - proper datasource uniqueness-based dup detection 1278 | #has_dupes = False 1279 | #all_names = [self.canvas.layer(i).name() for i in layer_list] 1280 | #if not len(all_names) == len(set(all_names)): 1281 | # has_dupes = True 1282 | warn_about_v2 = False 1283 | l_names = [] 1284 | labeled_layer_cache = [] 1285 | idx = 1 1286 | for i in layer_list: 1287 | # get the qgis layer 1288 | q_lyr = self.canvas.layer(i) 1289 | if hasattr(q_lyr,'isUsingRendererV2') and q_lyr.isUsingRendererV2(): 1290 | # we don't support symbology-ng yet, so skip layer 1291 | warn_about_v2 = True 1292 | continue 1293 | 1294 | # wrap the qgis layer in the adapter class to we can quickly 1295 | # make sense of it for turning into a mapnik layer 1296 | lyr_a = LayerAdaptor(self.parent, 1297 | q_lyr, 1298 | self.scale_factor, 1299 | self.base_path, 1300 | self.raster_scale_factor(), 1301 | self.background 1302 | ) 1303 | # if the layer can be turned into a mapnik datasource... 1304 | if lyr_a.is_valid(): 1305 | # get a simple, non unique layer name 1306 | name = lyr_a.name() 1307 | # unless this layer is a duplicate we'll plan to 1308 | # add it as a mapnik layer 1309 | add_layer = True 1310 | if name in l_names: 1311 | # we have a duplicate layer (by name) 1312 | # thus we need to assign a unique name 1313 | 1314 | #### TODO make dup sniffing smarter and not 1315 | #### name dependent but datasource specific 1316 | #### to allow for renaming in the qgis legend 1317 | #### without breaking this trickery 1318 | 1319 | name += str(idx) 1320 | idx += 1 1321 | # if duplicate layers are to be interpreted as the user wishing to 1322 | # be able to create multiple styles per mapnik layer... 1323 | # then we want to 'merge' the layers by aggregating all possible styles 1324 | # against on mapnik layer, thus skipping any duplicate qgis layers 1325 | if self.merge_duplicate_layers: 1326 | add_layer = False 1327 | l_names.append(lyr_a.name()) 1328 | # create the style name from the unique layer name 1329 | style_name = '%s_style' % name 1330 | # the style may be created at different points below 1331 | # so we'll make it None for now... 1332 | style_obj = None 1333 | # TODO - the mapnik layer may not need to be created if it has a style with pen/brush = false 1334 | m_lyr = lyr_a.to_mapnik(name) 1335 | if add_layer: 1336 | # if we have a new layer to add 1337 | if self.merge_duplicate_layers:# and has_dupes: 1338 | # overwrite mapnik layer ignoring any possible scale visibility 1339 | # applied to the layer because we are going to aggregate all styles 1340 | # and apply the visibility to the mapnik style rules instead 1341 | #### TODO - implement when we have a better way of detecting dups 1342 | pass 1343 | #m_lyr = lyr_a.to_mapnik(name,scale_based=False) 1344 | # attach the style by name 1345 | m_lyr.styles.append(style_name) 1346 | # attach the layer to the map 1347 | 1348 | else: 1349 | # get existing layer and append new style 1350 | exists = [l for l in m.layers if l.name == lyr_a.name()] 1351 | if len(exists) == 1: 1352 | existing_layer = exists[0] 1353 | existing_layer.styles.append(style_name) 1354 | if lyr_a.scale_based: 1355 | # apply layers scales to style's rules 1356 | #### TODO - implement when we have a better way of detecting dups 1357 | pass 1358 | #min_scale,max_scale = lyr_a.get_min_max_scales() 1359 | #style_obj = lyr_a.get_style(min_scale=min_scale,max_scale=max_scale) 1360 | if not style_obj: 1361 | # don't apply layer scales to style rules 1362 | # just get the style as it is 1363 | style_obj = lyr_a.get_style() 1364 | 1365 | # finally, add this layers style to the map 1366 | m.append_style(style_name,style_obj) 1367 | 1368 | # now focus on text labels 1369 | if lyr_a.has_labels: 1370 | label_name, label_obj = '%s_labels' % name, lyr_a.get_label_style() 1371 | if layer_count == 1: 1372 | # if only one layer is in the project then there is no need 1373 | # apply to a separate layer and we can avoid cacheing 1374 | # so place right now on the current layer which should exist 1375 | # because it cannot be a duplicate 1376 | 1377 | # TODO - foreseeably the layer could have not main styles (brush,pen = false) 1378 | # and we only want to create labels 1379 | #if not m_lyr: 1380 | # m_lyr = lyr_a.to_mapnik(name) 1381 | m_lyr.styles.append(label_name) 1382 | m.append_style(label_name,label_obj) 1383 | elif layer_count > 1: 1384 | # now, for all layers with labels, create a new, duplicate 1385 | # mapnik layer with a text style, and cache it to enable 1386 | # appending it at the end to ensure the labels are on top 1387 | m_label_lyr = lyr_a.to_mapnik('%s_label_overlay' % name) 1388 | # note: layer based scale visibility left unchanged for label layers... 1389 | m_label_lyr.styles.append(label_name) 1390 | m.append_style(label_name,label_obj) 1391 | labeled_layer_cache.append(m_label_lyr) 1392 | 1393 | # finally append any new layer 1394 | if add_layer: 1395 | m.layers.append(m_lyr) 1396 | 1397 | # attach any cached label layers last (on top) 1398 | for layer in labeled_layer_cache: 1399 | m.layers.append(layer) 1400 | 1401 | if warn_about_v2 and INCOMPATIBLE_RENDERER_WARNING: 1402 | self.message('The "New Symbology" plugin in QGIS is not yet supported by Quantumnik and layers using it will be rendered blank. See http://bitbucket.org/springmeyer/quantumnik/issue/23 to track progress.') 1403 | INCOMPATIBLE_RENDERER_WARNING = False 1404 | return m 1405 | -------------------------------------------------------------------------------- /text_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | from PyQt4.QtCore import * 6 | from PyQt4.QtGui import * 7 | from text_editor_ui import Ui_DockWidget 8 | 9 | class TextEditor(QDockWidget, Ui_DockWidget): 10 | def __init__(self, parent=None): 11 | QDockWidget.__init__(self, parent.iface.mainWindow()) 12 | # Set up the user interface from Designer. 13 | self.parent = parent 14 | self.setupUi(self) 15 | #self.textEdit.setText("hello") 16 | 17 | def closeEvent(self, event): 18 | self.parent.dock_window = None -------------------------------------------------------------------------------- /text_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | DockWidget 3 | 4 | 5 | Qt::NonModal 6 | 7 | 8 | true 9 | 10 | 11 | 12 | 0 13 | 0 14 | 548 15 | 556 16 | 17 | 18 | 19 | 20 | 10 21 | 22 | 23 | 24 | Qt::TabFocus 25 | 26 | 27 | Qt::PreventContextMenu 28 | 29 | 30 | false 31 | 32 | 33 | false 34 | 35 | 36 | QDockWidget::AllDockWidgetFeatures 37 | 38 | 39 | Qt::AllDockWidgetAreas 40 | 41 | 42 | Mapnik Xml View 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 0 51 | 0 52 | 53 | 54 | 55 | false 56 | 57 | 58 | QFrame::Plain 59 | 60 | 61 | 0 62 | 63 | 64 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 65 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 66 | p, li { white-space: pre-wrap; } 67 | </style></head><body style=" font-family:'Lucida Grande'; font-size:10pt; font-weight:400; font-style:normal;"> 68 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;"></p></body></html> 69 | 70 | 71 | false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 2 86 | 87 | 88 | 2 89 | 90 | 91 | false 92 | 93 | 94 | false 95 | 96 | 97 | true 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /text_editor_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'text_editor.ui' 4 | # 5 | # Created: Sun Nov 15 17:09:47 2009 6 | # by: PyQt4 UI code generator 4.6.1 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | class Ui_DockWidget(object): 13 | def setupUi(self, DockWidget): 14 | DockWidget.setObjectName("DockWidget") 15 | DockWidget.setWindowModality(QtCore.Qt.NonModal) 16 | DockWidget.setEnabled(True) 17 | DockWidget.resize(548, 556) 18 | font = QtGui.QFont() 19 | font.setPointSize(10) 20 | DockWidget.setFont(font) 21 | DockWidget.setFocusPolicy(QtCore.Qt.TabFocus) 22 | DockWidget.setContextMenuPolicy(QtCore.Qt.PreventContextMenu) 23 | DockWidget.setAcceptDrops(False) 24 | DockWidget.setFloating(False) 25 | DockWidget.setFeatures(QtGui.QDockWidget.AllDockWidgetFeatures) 26 | DockWidget.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) 27 | self.dockWidgetContents = QtGui.QWidget() 28 | self.dockWidgetContents.setObjectName("dockWidgetContents") 29 | self.gridLayout = QtGui.QGridLayout(self.dockWidgetContents) 30 | self.gridLayout.setObjectName("gridLayout") 31 | self.textEdit = QtGui.QTextEdit(self.dockWidgetContents) 32 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) 33 | sizePolicy.setHorizontalStretch(0) 34 | sizePolicy.setVerticalStretch(0) 35 | sizePolicy.setHeightForWidth(self.textEdit.sizePolicy().hasHeightForWidth()) 36 | self.textEdit.setSizePolicy(sizePolicy) 37 | self.textEdit.setAcceptDrops(False) 38 | self.textEdit.setFrameShadow(QtGui.QFrame.Plain) 39 | self.textEdit.setLineWidth(0) 40 | self.textEdit.setAcceptRichText(False) 41 | self.textEdit.setObjectName("textEdit") 42 | self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1) 43 | DockWidget.setWidget(self.dockWidgetContents) 44 | 45 | self.retranslateUi(DockWidget) 46 | QtCore.QMetaObject.connectSlotsByName(DockWidget) 47 | 48 | def retranslateUi(self, DockWidget): 49 | DockWidget.setWindowTitle(QtGui.QApplication.translate("DockWidget", "Mapnik Xml View", None, QtGui.QApplication.UnicodeUTF8)) 50 | self.textEdit.setHtml(QtGui.QApplication.translate("DockWidget", "\n" 51 | "\n" 54 | "

", None, QtGui.QApplication.UnicodeUTF8)) 55 | 56 | --------------------------------------------------------------------------------