├── .gitattributes ├── .gitignore ├── README.md ├── USER_MANUAL.md ├── developers ├── Auto document │ ├── create_docu_developers.py │ └── create_docu_users.py ├── future_to_do_list.txt └── history_changelog.txt ├── examples ├── batch_add_to_map_example.py ├── pointmap_example.py ├── polygonmap_example.py └── quickview_example.py ├── geovis ├── __init__.py ├── colour.py ├── guihelper.py ├── listy.py ├── messages.py ├── shapefile_fork.py ├── textual.py └── timetaker.py ├── images ├── PIL_normal(old)_vs_antialiased(new).png └── readme_topbanner.png └── license.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | __private__/ 203 | .installed.cfg 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Geographic Visualizer (GeoVis) 2 | 3 | ![Map image of global provinces rendered with GeoVis (Data source: GADM v2)](https://raw.github.com/karimbahgat/geovis/master/images/readme_topbanner.png) 4 | 5 | **Version: 0.2.0** 6 | 7 | **Author: [Karim Bahgat](https://uit.academia.edu/KarimBahgat)** 8 | 9 | 10 | ## Introduction 11 | Python Geographic Visualizer (GeoVis) is a standalone geographic visualization module for the Python programming language intended for easy everyday-use by novices and power-programmers alike. It has one-liners for quickly visualizing a shapefile, building and styling basic maps with multiple shapefile layers, and/or saving to imagefiles. Uses the built-in Tkinter or other third-party rendering modules to do its main work. The current version is functional, but should be considered a work in progress with potential bugs, so use with care. For now, only visualizes shapefiles that are in lat/long unprojected coordinate system. 12 | 13 | 14 | ## Latest News 15 | 16 | - April 15, 2014: New v0.2.0 released, major update. 17 | - New features: 18 | - Symbolize shapefiles based on their attributes and add legend (categorical, equal classes, equal interval, natural breaks) 19 | - Choose between multiple symbolizer icons: circle, square, pyramid 20 | - Zoom the map to a region of interest 21 | - Manually draw basic geometries and write text on map 22 | - Functions that allow mid-script interactive user-inputs 23 | - Support for the Pillow version of PIL, and improved PIL quality by using antialiasing 24 | - Changed to MIT license to be more contributor friendly 25 | - Incompatible changes with previous version 26 | - Make and style a layer, then add to map, instead of adding to map directly from file 27 | - Color now has to be explicitly set and is no longer random by default 28 | - Support for PyCairo has been temporarily discontinued due to some difficulties, so is likely to have errors 29 | - Errors fixed 30 | - Fixed error with interactively saving image from the viewer window in PIL mode. 31 | - 23 Feb. 2014: A little too quick on the trigger finger for the first release. 32 | - Discovered and fixed a crucial import error. - Also expanded the README file and converted it to markdown, and added autogenerated Wiki documentation. 33 | - 21 Feb. 2014: First rough version of code released (v0.1.0). 34 | - Basic functionality 35 | - One liners for shapefile viewing and map saving 36 | - Multiple layers custom map creation 37 | - Customize each shapefile with colors and fillsize 38 | 39 | 40 | ## Table of Contents 41 | - [About](#about) 42 | - [Dependencies](#dependencies) 43 | - [System Compatibility](#system-compatibility) 44 | - [License](#license) 45 | - [Getting Started](#getting-started) 46 | - [Installation](#installation) 47 | - [Importing](#importing) 48 | - [Instant Mapping](#instant-mapping) 49 | - [Customized Maps](#customized-maps) 50 | - [Advanced Usage](#advanced-usage) 51 | - [Choosing Your Renderer](#choosing-your-renderer) 52 | - [Zooming In](#zooming-in) 53 | - [Playing With Colors](#playing-with-colors) 54 | - [Batch Map Creation](#batch-map-creation) 55 | - [Help and Documentation](#help-and-documentation) 56 | - [Contributing](#contributing) 57 | - [Thanks To](#thanks-to) 58 | 59 | 60 | ## About 61 | 62 | ### Dependencies 63 | Technically speaking, GeoVis has no external dependencies, but it is highly recommended that you install the [Aggdraw](http://effbot.org/zone/aggdraw-index.htm), [PIL](http://www.pythonware.com/products/pil/) or [PyCairo](http://cairographics.org/pycairo/) renderer libraries to do the rendering. GeoVis automatically detects which renderer module you have and uses the first it finds in the following order (aggdraw, PIL, pycairo). If you wish to manually choose a different renderer this has to be specified for each session. If none of these are available then GeoVis will default to using the built-in Tkinter Canvas as its renderer, but due to major limitations this is not recommended for viewing larger shapefiles. 64 | 65 | ### System Compatibility 66 | Should work on Python version 2.x and Windows. Has not yet been tested on Python 3.x or other OS systems. 67 | 68 | ### License 69 | Contributors are wanted and needed, so this code is free to share, use, reuse, 70 | and modify according to the MIT license, see license.txt 71 | 72 | 73 | ## Getting Started 74 | 75 | ### Installation 76 | 1. Download GeoVis, from either a or b: 77 | - a) The most recent stable version from [the releases page](https://github.com/karimbahgat/GeoVis/releases) (Recommended). 78 | - b) The "bleeding edge" version using the "download zip" button to the right of the main repository folder. 79 | 2. Install it, using either a or b: 80 | - a) Place the geovis folder in your Python site-packages folder for a "permanent" installation. 81 | - b) Or begin each session by typing `import sys` and `sys.path.append("folder path where the geovis folder is located")`. 82 | 83 | ### Importing 84 | Assuming you have already installed it as described in the Installation section, GeoVis is imported as: 85 | 86 | ```python 87 | import geovis 88 | ``` 89 | 90 | To begin using geovis either check out the full list of commands in the [the USER_MANUAL](../master/USER_MANUAL.md), or keep reading below for a basic introduction. 91 | 92 | ### Instant Mapping 93 | If you are simply wanting to inspect some shapefile interactively, or for seeing how your processed shapefile turned out, then you do this with a simple one-liner: 94 | 95 | ```python 96 | geovis.ViewShapefile("C:/shapefile.shp") 97 | ``` 98 | 99 | If you quickly want to show someone else your shapefile over email or on a forum you can just as easily save you map to an image either by clicking the "save image" button in the interactive window viewer or with the following line: 100 | 101 | ```python 102 | geovis.SaveShapefileImage("C:/shapefile.shp", 103 | savepath="C:/output_picture.png") 104 | ``` 105 | 106 | ### Customized Maps 107 | It is also possible to build your map from scratch in order to create a more visually appealing map. 108 | 109 | First setup and create a newmap instance: 110 | 111 | ```python 112 | geovis.SetMapDimensions(width=4000, height=2000) 113 | geovis.SetMapBackground(geovis.Color("blue") 114 | newmap = geovis.NewMap() 115 | ``` 116 | 117 | Next, each shapefile has to be loaded and symbolized into layer instances: 118 | 119 | ```python 120 | polylayer = geovis.Layer(polypath, fillcolor=geovis.Color("random")) 121 | pointlayer = geovis.Layer(pointpath, fillcolor=geovis.Color("random")) 122 | ``` 123 | 124 | If you wish to, you can also visualize the underlying attributes of your layer by adding one or more classifications. For instance, out point layer can be made to vary from small to large size and green to red color based on its "population" attribute: 125 | 126 | ```python 127 | pointlayer.AddClassification(symboltype="fillsize", valuefield="population", symbolrange=[0.01,0.2], classifytype="natural breaks", nrclasses=5) 128 | pointlayer.AddClassification(symboltype="fillcolor", valuefield="population", symbolrange=[geovis.Color("green"),geovis.Color("red")], classifytype="natural breaks", nrclasses=5) 129 | ``` 130 | 131 | In which case you will probably want to add a legend so that you can see which symbols represent which values. Let's place it in the top left corner of the map: 132 | 133 | ```python 134 | pointlayer.AddLegend(layer=pointlayer, upperleft=[0.01,0.01], bottomright=[0.3,0.3]) 135 | ``` 136 | 137 | Finally, render the layers to the newmap instance: 138 | 139 | ```python 140 | newmap.AddToMap(polylayer) 141 | newmap.AddToMap(pointlayer) 142 | ``` 143 | 144 | For a finishing touch, add a top-centered map title: 145 | 146 | ```python 147 | newmap.AddText(relx=0.5, rely=0.01, text="Example Map Title", textsize=0.10, textanchor="n") 148 | ``` 149 | 150 | And save the map: 151 | 152 | ```python 153 | newmap.SaveMap("C:/Users/BIGKIMO/Desktop/custombuiltmap.png") 154 | ``` 155 | 156 | 157 | ## Advanced Usage 158 | 159 | ### Choosing Your Renderer 160 | If you have more than one renderer and you want to choose which one to use, for instance PIL, you must do this at the beginning of each session (also, if you're going for maximum speed/less quality then enable reducing the number of vectors while you're at it, though this is not recommended for line-shapefiles): 161 | 162 | ```python 163 | geovis.SetRenderingOptions(renderer="PIL", reducevectors=True) 164 | ``` 165 | 166 | ### Zooming In 167 | By default, the map you create will show the entire world. To zoom the map to a particular area or region of interest you simply set the zoomextents of the map, which must be done before you add your layers: 168 | 169 | ```python 170 | geovis.SetMapZoom(x2x=[-90,90], y2y=[-45,45]) 171 | ``` 172 | 173 | ### Playing With Colors 174 | There are several ways to play with the colors in your map. The most basic stylizing tool you will want to know about is the Color creator (a wrapper around [Valentin Lab's Colour module](https://pypi.python.org/pypi/colour) with added convenience functionality). You can either create a random color: 175 | 176 | ```python 177 | randomcolor = geovis.Color("random") 178 | ``` 179 | 180 | Or you can create a specific color the way you imagine it in your head by writing the color name and optionally tweaking the color intensity and brightness (float value between 0 and 1). Let's create a strong (high intensity) but dark (low brightness) red: 181 | 182 | ```python 183 | strongdarkred = geovis.Color("red", intensity=0.8, brightness=0.2) 184 | ``` 185 | 186 | Alternatively, instead of creating a very specific color you can create a random color that still keeps within certain limits. For instance, specifying a low brightness value and low intensity value but not specifying a color name will produce a random matte-looking color. Better yet, you can set the style argument to "matte" (among many other style names, see the documentation for the full list) which automatically chooses the brightness and intensity for you: 187 | 188 | ```python 189 | randommattecolor = geovis.Color(style="matte") 190 | ``` 191 | 192 | Assuming you now know how to set your own colors or color styles, these colors are useful since they can be used to specify the color of any number of symbol options passed as keyword arguments to GeoVis' various rendering functions (see the documentation for a full list of changable symbol options). For instance, let's save a shapefile image as before, but this time set the fillcolor of the shapefile polygons/lines/circles to our strong-dark-red that we defined previously. In addition we will increase the outline width to match the strong fillcolor (we leave the outline *color* to its defaul black since this fits with the map): 193 | 194 | ```python 195 | geovis.SaveShapefileImage("C:/shapefile.shp", 196 | savepath="C:/output_picture.png", 197 | fillcolor=strongdarkred, 198 | outlinewidth=5) 199 | ``` 200 | 201 | ### Batch Map Creation 202 | Sometimes it is necessary to quickly create a gallery of images of all your shapefiles in a given directory. GeoVis provides a general utility tool that can help you do this; it loops through, parses, and returns the foldername, filename, and extension of all shapefiles in a folder tree, which in turn can be used as input for the SaveShapefileImage function. So for instance we may write it as: 203 | 204 | ```python 205 | for eachfolder, eachshapefile, eachfiletype in geovis.ShapefileFolder(r"D:\Test Data\GPS"): 206 | shapefilepath = eachfolder+eachshapefile+eachfiletype 207 | savetopath = "C:/Users/BIGKIMO/Desktop/mapgallery/"+eachshapefile+".png" 208 | geovis.SaveShapefileImage(shapefilepath, savetopath) 209 | ``` 210 | 211 | The filename, parent-folder, and file extension can be played around with to do many other batch operations as well, such as placing each map image next to (in the same folder as) each shapefile. 212 | 213 | 214 | ## Help and Documentation 215 | This brief introduction has only covered the most essential functionality of GeoVis. 216 | If you need more information about or are experiencing problems with a particular function you can look up the full documentation of available functions, classes, and arguments by typing `help(geovis)`, or checking out [the USER_MANUAL](../master/USER_MANUAL.md). 217 | 218 | If you still need help, you can either [submit an issue](https://github.com/karimbahgat/geovis/issues) here on GitHub, or contact me directly at: karim.bahgat.norway@gmail.com 219 | 220 | 221 | ## Contributing 222 | I welcome any efforts at contributing to this project. Below are a list of current problems and limitations that I hope to change in future releases: 223 | 224 | - The shapefiles have to be in lat/long coordinates (i.e. unprojected) in order to be displayed correctly. That is, GeoVis does not yet handle projections or coordinate transformation. 225 | - Currently, zooming to a local scale can be made much more efficient by using spatial indexing such as QuadTree... 226 | - I have not yet figured out how to pass PyCairo rendered images to the Tkinter window for viewing, so for now it results in an error. 227 | - Currently has not been tested with, and probably won't work in Python 3.x due to syntax changes, so should look into fixing this. 228 | 229 | --- 230 | 231 | --- 232 | 233 | --- 234 | 235 | ### Thanks To 236 | GeoVis could not have been accomplished without the excellent work of other open-source modules which it uses behind-the-scenes: 237 | 238 | - Several contributors over at Stackoverflow for their help with various rendering related problems and optimizations. 239 | - Various bloggers whose code examples have in some cases been used directly in GeoVis (these parts of the code are marked with the original source) 240 | - For shapefile reading it uses a modified version of 241 | [Joel Lawhead's PyShp module](http://code.google.com/p/pyshp/). 242 | - For color-wizardry it builds on and expands [Valentin Lab's Colour module](https://pypi.python.org/pypi/colour). 243 | - And offcourse it relies on the rendering of whichever renderer you are using. 244 | 245 | -------------------------------------------------------------------------------- /USER_MANUAL.md: -------------------------------------------------------------------------------- 1 | # User Manual for Python Geographic Visualizer (GeoVis) 2 | 3 | **Version: 0.2.0** 4 | 5 | **Date: April 15, 2014** 6 | 7 | **Author: [Karim Bahgat](https://uit.academia.edu/KarimBahgat)** 8 | 9 | **Contact: karim.bahgat.norway@gmail.com** 10 | 11 | **Homepage: https://github.com/karimbahgat/geovis** 12 | 13 | ## Table of Contents 14 | 15 | - [About](#about) 16 | - [System Compatibility](#system-compatibility) 17 | - [Dependencies](#dependencies) 18 | - [License](#license) 19 | - [How GeoVis Works](#how-geovis-works) 20 | - [Usage Philosophy](#usage-philosophy) 21 | - [Screen Coordinate System](#screen-coordinate-system) 22 | - [Stylizing Options](#stylizing-options) 23 | - [Text Options](#text-options) 24 | - [Available Text Fonts](#available-text-fonts) 25 | - [Functions and Classes](#functions-and-classes) 26 | - [geovis.AskColor](#geovisaskcolor) 27 | - [geovis.AskFieldName](#geovisaskfieldname) 28 | - [geovis.AskNumber](#geovisasknumber) 29 | - [geovis.AskShapefilePath](#geovisaskshapefilepath) 30 | - [geovis.AskString](#geovisaskstring) 31 | - [geovis.Color](#geoviscolor) 32 | - [geovis.Layer](#geovislayer----class-object) 33 | - [.AddClassification](#addclassification) 34 | - [geovis.NewMap](#geovisnewmap----class-object) 35 | - [.AddLegend](#addlegend) 36 | - [.AddShape](#addshape) 37 | - [.AddText](#addtext) 38 | - [.AddToMap](#addtomap) 39 | - [.DrawCircle](#drawcircle) 40 | - [.DrawLine](#drawline) 41 | - [.DrawRectangle](#drawrectangle) 42 | - [.SaveMap](#savemap) 43 | - [.ViewMap](#viewmap) 44 | - [geovis.SaveShapefileImage](#geovissaveshapefileimage) 45 | - [geovis.SetMapBackground](#geovissetmapbackground) 46 | - [geovis.SetMapDimensions](#geovissetmapdimensions) 47 | - [geovis.SetMapZoom](#geovissetmapzoom) 48 | - [geovis.SetRenderingOptions](#geovissetrenderingoptions) 49 | - [geovis.Shapefile](#geovisshapefile----class-object) 50 | - [.ClearSelection](#clearselection) 51 | - [.InvertSelection](#invertselection) 52 | - [.SelectByQuery](#selectbyquery) 53 | - [geovis.ShapefileFolder](#geovisshapefilefolder) 54 | - [geovis.ViewShapefile](#geovisviewshapefile) 55 | 56 | ## About 57 | 58 | Python Geographic Visualizer (GeoVis) is a standalone geographic visualization 59 | module for the Python programming language intended for easy everyday-use by 60 | novices and power-programmers alike. It has one-liners for quickly visualizing 61 | a shapefile, building and styling basic maps with multiple shapefile layers, 62 | and/or saving to imagefiles. Uses the built-in Tkinter or other third-party 63 | rendering modules to do its main work. The current version is functional, but 64 | should be considered a work in progress with potential bugs, so use with care. 65 | For now, only visualizes shapefiles that are in lat/long unprojected coordinate 66 | system. 67 | 68 | ### System Compatibility 69 | 70 | Should work on Python version 2.x and Windows. Has not yet been tested on 71 | Python 3.x or other OS systems. 72 | 73 | ### Dependencies 74 | 75 | Technically speaking, GeoVis has no external dependencies, but it is highly 76 | recommended that you install the [Aggdraw](http://effbot.org/zone/aggdraw-index.htm), 77 | [PIL](http://www.pythonware.com/products/pil/) or [PyCairo](http://cairographics.org/pycairo/) 78 | renderer libraries to do the rendering. GeoVis automatically detects which 79 | renderer module you have and uses the first it finds in the following order 80 | (aggdraw, PIL, pycairo). If you wish to manually choose a different renderer 81 | this has to be specified for each session. If none of these are available then 82 | GeoVis will default to using the built-in Tkinter Canvas as its renderer, but 83 | due to major limitations this is not recommended for viewing larger shapefiles. 84 | 85 | ### License 86 | 87 | Contributors are wanted and needed, so this code is free to share, use, reuse, 88 | and modify according to the MIT license, see license.txt 89 | 90 | ## How GeoVis works 91 | 92 | The following section describes some general info and options about how 93 | GeoVis works. 94 | 95 | ### Usage Philosophy 96 | 97 | The general philosophy of GeoVis is that it should be easy to both learn 98 | and use for end-users, particularly for people who are new to programming. 99 | More specifically: 100 | 101 | - It should be logical and intuitive what commands to use. 102 | - Making a simple map should require relatively few lines of code. 103 | - The user should only have to learn and deal with a few basic commands. 104 | - All command names use full wording and first-letter uppercasing 105 | of each word for easy identification, ala the Arcpy syntax. 106 | 107 | The precise commands and arguments to use can be looked up in the 108 | documentation. Using these the general steps to follow are: 109 | 110 | 1. Create a new map 111 | 2. Create and symbolize layers of geographical data 112 | 3. Add the layers to the map 113 | 4. View or save the map 114 | 115 | ### Screen Coordinate system 116 | 117 | Many of the rendering methods let the user to specify one or more 118 | locations in relative screen coordinates. These screen coordinates 119 | are given as x and y values with a float between 0 and 1. The relative 120 | coordinates (0,0) places something in the upper left corner of the 121 | screen, while (1,1) places it in the bottom right corner. 122 | 123 | ### Stylizing Options 124 | 125 | Styling a map layer is done by setting one or more keyword arguments 126 | during the creation of the Layer class. The same styling keywords can 127 | also be used when manually drawing shapes and figures on a map (the ones 128 | offering the "customoptions" argument option). 129 | 130 | | __option__ | __description__ 131 | | --- | --- 132 | | fillsize | the size of a circle, square, pyramid, or the thickness of a line. Has no effect on polygon shapes. Given as proportion of the map size, so that a circle of size 0.10 will cover about 10 percent of the map. A float between 0 and 1 133 | | fillwidth | currently only used for the width of a pyramid when using the pyramid symbolizer. Given as proportion of the map size. A float between 0 and 1 134 | | fillheight | currently has no effect 135 | | fillcolor | the hex color of the fill 136 | | outlinewidth | the width of the outline if any, given as proportion of the fillsize. A float between 0 and 1 137 | | outlinecolor | the hex color of the outline 138 | 139 | ### Text Options 140 | 141 | When adding text to a map one can use one or more of the following 142 | keyword arguments: 143 | 144 | | __option__ | __description__ 145 | | --- | --- 146 | | textfont | the name of the textfont to use; available textfonts vary depending on the renderer being used, see list below. 147 | | textsize | the size of the text, given as percent pixelheight of the map dimensions (eg 0.20 being a really large text with a size of about 20 percent of the map) 148 | | textcolor | the hex color string of the text 149 | | textopacity | currently not being used 150 | | texteffect | currently not being used 151 | | textanchor | what area of the text to use as the anchor point when placing it, given as one of the following compass direction strings: center, n, ne, e, se, s, sw, w, nw 152 | | textboxfillcolor | the fillcolor of the text's bounding box, if any (default is None, meaning no bounding box) 153 | | textboxoutlinecolor | the outlinecolor of the bounding box, if any (default is None, meaning no bounding box outline) 154 | | textboxfillsize | proportional size of the text's bounding box relative to the textsize (eg 1.10 gives the bounding box about a 10 percent padding around the text, default is 1.10) 155 | | textboxoutlinewidth | width of the textbox outline as percent of the textboxfilling (eg 1 gives a 1 percent outline width) 156 | | textboxopacity | currently not being used 157 | 158 | ### Available Text Fonts 159 | 160 | Only a few basic text fonts are currently supported by each renderer. 161 | They are: 162 | 163 | - Tkinter 164 | - times new roman 165 | - courier 166 | - helvetica 167 | - PIL 168 | - times new roman 169 | - arial 170 | - Aggdraw 171 | - times new roman 172 | - arial 173 | - PyCairo 174 | - serif 175 | - sans-serif 176 | - cursive 177 | - fantasy 178 | - monospace 179 | 180 | 181 | ## Functions and Classes 182 | 183 | ### geovis.AskColor(...): 184 | Pops up a temporary tk window asking user to visually choose a color. 185 | Returns the chosen color as a hex string. Also prints it as text in case 186 | the user wants to remember which color was picked and hardcode it in the script. 187 | 188 | | __option__ | __description__ 189 | | --- | --- 190 | | *text | an optional string to identify what purpose the color was chosen for when printing the result as text. 191 | 192 | ### geovis.AskFieldName(...): 193 | Loads and prints the available fieldnames of a shapefile, and asks the user which one to choose. 194 | Returns the chosen fieldname as a string. 195 | 196 | | __option__ | __description__ 197 | | --- | --- 198 | | *text | an optional string to identify for what purpose the chosen fieldname will be used. 199 | 200 | ### geovis.AskNumber(...): 201 | Asks the user to interactively input a number (float or int) at any point in the script, and returns the input number. 202 | 203 | | __option__ | __description__ 204 | | --- | --- 205 | | *text | an optional string to identify for what purpose the chosen number will be used. 206 | 207 | ### geovis.AskShapefilePath(...): 208 | Pops up a temporary tk window asking user to visually choose a shapefile. 209 | Returns the chosen shapefile path as a text string. Also prints it as text in case 210 | the user wants to remember which shapefile was picked and hardcode it in the script. 211 | 212 | | __option__ | __description__ 213 | | --- | --- 214 | | *text | an optional string to identify what purpose the shapefile was chosen for when printing the result as text. 215 | 216 | ### geovis.AskString(...): 217 | Asks the user to interactively input a string at any point in the script, and returns the input string. 218 | 219 | | __option__ | __description__ 220 | | --- | --- 221 | | *text | an optional string to identify for what purpose the chosen string will be used. 222 | 223 | ### geovis.Color(...): 224 | Returns a hex color string of the color options specified. 225 | NOTE: New in v0.2.0, basecolor, intensity, and brightness no longer defaults to random, and it is no longer possible to call an empty Color() function (a basecolor must now always be specified). 226 | 227 | | __option__ | __description__ | __input__ 228 | | --- | --- | --- 229 | | basecolor | the human-like name of a color. Always required, but can also be set to 'random'. | string 230 | | *intensity | how strong the color should be. Must be a float between 0 and 1, or set to 'random' (by default uses the 'strong' style values, see 'style' below). | float between 0 and 1 231 | | *brightness | how light or dark the color should be. Must be a float between 0 and 1 , or set to 'random' (by default uses the 'strong' style values, see 'style' below). | float between 0 and 1 232 | | *style | a named style that overrides the brightness and intensity options (optional). | For valid style names, see below. 233 | 234 | Valid style names are: 235 | 236 | - 'strong' 237 | - 'dark' 238 | - 'matte' 239 | - 'bright' 240 | - 'pastelle' 241 | 242 | ### geovis.Layer(...) --> class object 243 | Creates and returns a thematic layer instance (a visual representation of a geographic file) that can be symbolized and used to add to a map. 244 | 245 | | __option__ | __description__ 246 | | --- | --- 247 | | filepath | the path string of the geographic file to add, including the file extension. 248 | | **customoptions | any series of named arguments of how to style the shapefile visualization (optional). Valid arguments are: fillcolor, fillsize (determines the circle size for point shapefiles, line width for line shapefiles, and has no effect for polygon shapefiles), outlinecolor, outlinewidth. For more info see the special section on how to stylize a layer. 249 | 250 | - #### .AddClassification(...): 251 | Adds a classification/instruction to the layer on how to symbolize a particular symbol part (e.g. fillcolor) based on a shapefile's attribute values. 252 | 253 | | __option__ | __description__ | __input__ 254 | | --- | --- | --- 255 | | symboltype | a string indicating which type of symbol the classification should apply to. | any of: "fillsize", "fillwidth", "fillheight", "fillcolor", "outlinewidth", "outlinecolor" 256 | | valuefield | a string with the name of a shapefile attribute field whose values will be used to inform the classification. | string 257 | | symbolrange | a list or tuple of the range of symbol values that should be used for the symbol type being classified. You only need to assign the edge/breakpoints in an imaginary gradient of symbol values representing the transition from low to high value classes; the values in between will be interpolated if needed. The symbol values must be floats or integers when classifying a size-based symbol type, or hex color strings when classifying a color-based symbol type. | list or tuple 258 | | classifytype | a string with the name of the mathematical algorithm used to calculate the break points that separate the classes in the attribute values. | For valid classification type names see list below 259 | | nrclasses | an integer or float for how many classes to subdivide the data and symbol values into. | Integer or float 260 | 261 | Valid names for the classifytype option are: 262 | 263 | - __"categorical"__ 264 | Assigns a unique class/symbol color to each unique attribute value, so can only be used when classifying color-based symbol types 265 | - __"equal classes"__ 266 | Makes sure that there are equally many features in each class, which means that features with the same attribute values can be found in multiple classes 267 | - __"equal interval"__ 268 | Classes are calculated so that each class only contains features that fall within a value range that is equally large for all classes 269 | - __"natural breaks"__ 270 | The Fisher-Jenks natural breaks algorithm, adapted from the Python implementation by Daniel J. Lewis (http://danieljlewis.org/files/2010/06/Jenks.pdf), is used to find 'natural' breaks in the shapefile dataset, i.e. where the value range within each class is as similar as possible and where the classes are as different as possible from each other. This algorithm is notorious for being slow for large datasets, so for datasets larger than 1000 records the calculation will be limited to a random sample of 1000 records (thanks to Carston Farmer for that idea, see: http://www.carsonfarmer.com/2010/09/adding-a-bit-of-classification-to-qgis/), and in addition that calculation will be performed 6 times, with the final break points being the sample mean of all the calculations. For large datasets this means that the natural breaks algorithm and the resultant map classification may turn out differently each time; however, the results should be somewhat consistent especially due to the random nature of the approach and the multiple sample means 271 | 272 | ### geovis.NewMap(...) --> class object 273 | Creates and returns a new map based on previously defined mapsettings. 274 | 275 | *Takes no arguments* 276 | 277 | - #### .AddLegend(...): 278 | Draws a basic legend for a given layer. 279 | 280 | | __option__ | __description__ 281 | | --- | --- 282 | | layer | the layer instance whose legend you wish to add to the map 283 | | upperleft | the upperleft corner of the legend as a list or tuple of the relative x and y position, each a float between 0-1 284 | | bottomright | the bottomright corner of the legend as a list or tuple of the relative x and y position, each a float between 0-1 285 | | legendtitle | the title of the legend as a string, by default uses the filename of the underlying shapefile 286 | | boxcolor | the hex color of the rectangle box that contains the legend, set to None to not render the box, default is a lightgray. 287 | | boxoutlinecolor | the hex color of the outline of the rectangle box that contains the legend, set to None to not render the outline, default is black. 288 | | boxoutlinewidth | the thickness of the boxoutline color relative to the box size, so 0.10 is 10 percent of the box size 289 | 290 | - #### .AddShape(...): 291 | This adds an individual shape instead of an entire file. 292 | 293 | | __option__ | __description__ 294 | | --- | --- 295 | | shapeobj | a shape instance, currently it only works with the PyShpShape instances that are returned when looping through the geovis Shapefile instance 296 | | **customoptions | any number of named arguments to style the shape 297 | 298 | - #### .AddText(...): 299 | Writes text on the map. 300 | 301 | | __option__ | __description__ 302 | | --- | --- 303 | | relx | the relative x position of the text's centerpoint, a float between 0-1 304 | | rely | the relative y position of the text's centerpoint, a float between 0-1 305 | | text | the text to add to the map, as a string 306 | | **customoptions | any number of named arguments to style the text 307 | 308 | - #### .AddToMap(...): 309 | Add and render a layer instance to the map. 310 | 311 | | __option__ | __description__ 312 | | --- | --- 313 | | layer | the layer instance that you wish to add to the map 314 | 315 | - #### .DrawCircle(...): 316 | Draws a circle on the map. 317 | 318 | | __option__ | __description__ 319 | | --- | --- 320 | | relx | the relative x position of the circle's centerpoint, a float between 0-1 321 | | rely | the relative y position of the circle's centerpoint, a float between 0-1 322 | | **customoptions | any number of named arguments to style the line 323 | 324 | - #### .DrawLine(...): 325 | Draws a line on the map. 326 | 327 | | __option__ | __description__ 328 | | --- | --- 329 | | startpos | a list or tuple of the relative x and y position where the line should start, each a float between 0-1 330 | | stoppos | a list or tuple of the relative x and y position where the line should end, each a float between 0-1 331 | | **customoptions | any number of named arguments to style the line 332 | 333 | - #### .DrawRectangle(...): 334 | Draws a rectangle on the map. 335 | 336 | | __option__ | __description__ 337 | | --- | --- 338 | | upperleft | the upperleft corner of the rectangle as a list or tuple of the relative x and y position, each a float between 0-1 339 | | bottomright | the bottomright corner of the rectangle as a list or tuple of the relative x and y position, each a float between 0-1 340 | | **customoptions | any number of named arguments to style the rectangle 341 | 342 | - #### .SaveMap(...): 343 | Save the map to an image file. 344 | 345 | | __option__ | __description__ 346 | | --- | --- 347 | | savepath | the string path for where you wish to save the map image. Image type extension must be specified ('.png','.gif',...) 348 | 349 | - #### .ViewMap(...): 350 | View the created map embedded in a Tkinter window. Map image can be panned, but not zoomed. Offers a 'save image' button to allow to interactively save the image. 351 | 352 | *Takes no arguments* 353 | 354 | ### geovis.SaveShapefileImage(...): 355 | Quick task to save a shapefile to an image. 356 | 357 | | __option__ | __description__ 358 | | --- | --- 359 | | shapefilepath | the path string of the shapefile. 360 | | savepath | the path string of where to save the image, including the image type extension. 361 | | **customoptions | any series of named arguments of how to style the shapefile visualization (optional). Valid arguments are: fillcolor, fillsize (determines the circle size for point shapefiles, line width for line shapefiles, and has no effect for polygon shapefiles), outlinecolor, outlinewidth. 362 | 363 | ### geovis.SetMapBackground(...): 364 | Sets the mapbackground of the next map to be made. At startup the mapbackground is transparent (None). 365 | 366 | | __option__ | __description__ 367 | | --- | --- 368 | | mapbackground | takes a hex color string, as can be created with the Color function. It can also be None for a transparent background (default). 369 | 370 | ### geovis.SetMapDimensions(...): 371 | Sets the width and height of the next map image. At startup the width and height are set to the dimensions of the window screen. 372 | 373 | | __option__ | __description__ 374 | | --- | --- 375 | | width | the pixel width of the final map image to be rendered, an integer. 376 | | height | the pixel height of the final map image to be rendered, an integer. 377 | 378 | ### geovis.SetMapZoom(...): 379 | Zooms the map to the given mapextents. 380 | 381 | | __option__ | __description__ 382 | | --- | --- 383 | | x2x | a two-item list of the x-extents in longitude format, from the leftmost to the rightmost longitude, default is full extent [-180, 180] 384 | | y2y | a two-item list of the y-extents in latitude format, from the bottommost to the topmost latitude, default is full extent [-90, 90] 385 | 386 | ### geovis.SetRenderingOptions(...): 387 | Sets certain rendering options that apply to all visualizations or map images. 388 | 389 | | __option__ | __description__ 390 | | --- | --- 391 | | *renderer | a string describing which Python module will be used for rendering. This means you need to have the specified module installed. Valid renderer values are 'aggdraw' (default), 'PIL', 'pycairo', 'tkinter'. Notes: If you have no renderers installed, then use Tkinter which comes with all Python installations, be aware that it is significantly slow, memory-limited, and cannot be used to save images. Currently PyCairo is not very well optimized, and is particularly slow to render line shapefiles. 392 | | *numpyspeed | specifies whether to use numpy to speed up shapefile reading and coordinate-to-pixel conversion. Must be True (default) or False. 393 | | *reducevectors | specifies whether to reduce the number of vectors to be rendered. This can speed up rendering time, but may lower the quality of the rendered image, especially for line shapefiles. Must be True or False (default). 394 | 395 | ### geovis.Shapefile(...) --> class object 396 | Opens and reads a shapefile. Supports looping through it to extract one PyShpShape instance at a time. Using it with a print() function passes the filename, and measuring its len() returns the number of rows. 397 | 398 | | __options__ | __description__ 399 | | --- | --- 400 | | shapefilepath | the filepath of the shapefile, including the .shp extension 401 | | showprogress | True if wanting to display a progressbar while looping through the shapefile (default), otherwise False (default) 402 | | progresstext | a textstring to print alongside the progressbar to help identify why it is being looped 403 | 404 | - #### .ClearSelection(...): 405 | Clears the current selection so that all shapes will be looped 406 | 407 | - #### .InvertSelection(...): 408 | Inverts the current selection 409 | 410 | - #### .SelectByQuery(...): 411 | Make a query selection on the shapefile so that only those features where the query evaluates to True are returned. 412 | 413 | | __option__ | __description__ 414 | | --- | --- 415 | | query | a string containing Python-like syntax (required). Feature values for fieldnames can be grabbed by specifying the fieldname as if it were a variable (case-sensitive). Note that evaluating string expressions is currently case-sensitive, which becomes particularly unintuitive for less-than/more-than alphabetic queries. 416 | | *inverted | a boolean specifying whether to invert the selection (default is False). 417 | 418 | ### geovis.ShapefileFolder(...): 419 | A generator that will loop through a folder and all its subfolder and return information of every shapefile it finds. Information returned is a tuple with the following elements (string name of current subfolder, string name of shapefile found, string of the shapefile's file extension(will always be '.shp')) 420 | 421 | | __option__ | __description__ 422 | | --- | --- 423 | | folder | a path string of the folder to check for shapefiles. 424 | 425 | ### geovis.ViewShapefile(...): 426 | Quick task to visualize a shapefile and show it in a Tkinter window. 427 | 428 | | __option__ | __description__ 429 | | --- | --- 430 | | shapefilepath | the path string of the shapefile. 431 | | **customoptions | any series of named arguments of how to style the shapefile visualization (optional). Valid arguments are: fillcolor, fillsize (determines the circle size for point shapefiles, line width for line shapefiles, and has no effect for polygon shapefiles), outlinecolor, outlinewidth. 432 | 433 | -------------------------------------------------------------------------------- /developers/Auto document/create_docu_developers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append(r"C:\Users\BIGKIMO\Dropbox\Work\Research\Software\Various Python Libraries\GitDoc") 3 | import gitdoc 4 | 5 | FILENAME = "geovis" 6 | FOLDERPATH = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 7 | OUTPATH = r"C:\Users\BIGKIMO\Documents\GitHub\geovis\developers" 8 | OUTNAME = "dev_documentation" 9 | EXCLUDETYPES = ["module"] 10 | gitdoc.Module2GitDown(FOLDERPATH, 11 | filename=FILENAME, 12 | outputfolder=OUTPATH, 13 | outputname=OUTNAME, 14 | excludetypes=EXCLUDETYPES, 15 | excludesecret=False, 16 | excludesupersecret=False 17 | ) 18 | -------------------------------------------------------------------------------- /developers/Auto document/create_docu_users.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append(r"C:\Users\BIGKIMO\Documents\GitHub\GitDoc") 3 | import gitdoc 4 | 5 | FILENAME = "geovis" 6 | FOLDERPATH = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 7 | OUTPATH = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 8 | OUTNAME = "USER_MANUAL" 9 | EXCLUDETYPES = ["module","variable"] 10 | gitdoc.DocumentModule(FOLDERPATH,filename=FILENAME,outputfolder=OUTPATH,outputname=OUTNAME,excludetypes=EXCLUDETYPES) 11 | -------------------------------------------------------------------------------- /developers/future_to_do_list.txt: -------------------------------------------------------------------------------- 1 | 2 | REMAINING TASKS UNTIL NEXT RELEASE: 3 | 4 | --- DONE! 5 | 6 | 7 | WAITING UNTIL NEXT TIME, ERRORS TO FIX: 8 | 9 | - Fix normal (non-numpy) mapcoords error... 10 | - Error with map/image proportions when zooming bc only the width has the priority and a long width can stretch image 11 | - CHECK OUT cntry_code categorical classification error 12 | - Make so if adding multiple classifications of same symboltype then replace the older one 13 | - Potential error with PIL if you installed PIL from source without libfreetype, as opposed to with an exe installer 14 | - Note that some parts of the symbology such as the outline may not always display as you might expect, since depending on the order of the shapefile features some shapes may be rendered on top of and blocking other shapes 15 | 16 | WAITING UNTIL NEXT TIME, FEATURES TO ADD: 17 | 18 | - ADD TO DOCS THAT PIL AND TKINTER DOES NOT SUPPORT OUTLINE WIDTH SO THEIR OUTLINES WILL ALWAYS BE 1 PIXEL THICK, SINCE THEY ONLY HAVE OUTLINE COLOR OPTION BUT NOT OUTLINEWIDTH OPTION... 19 | - ADD TO DOCS A CONTRIBUTORS SECTION OF WHAT CONTRIBUTORS CAN HELP WITH (EG NEW SYMBOLIZERS OR MAKING LINES SMOOTHER BY USING A DRAWPATH), AND A DRAWING IN THE CONTRIBUTORS SECTION OF THE INNER WORKINGS OF GEOVIS AND WHERE THINGS COME FROM/PASS BY/GO TO. 20 | - Figure out how add legend to basic shapefiles(nonclassified), and how manually customize everything? 21 | - Maybe add a little thing indicating the imagesize, and a paranthesis with text if image was too big for screen so has been downsized just for viewing purposes. 22 | - Is there a way to make classification sizes be mapped not linearly based on class rank, bur as percent of class midpoint compared to highest class midpoint (thus instead of interpolating symbolrange to nr of classes, interpolate to 100 percent and choose symbol index based on midpoint percent). Maybe that's what's known as proportional, except for all values instead of in classes. Use it by setting class2symbol="proportional" instead of "linear", or by saying proportional as classifytype. 23 | - MAYBE DISCONTINUE SUPPORT FOR PYCAIRO...? 24 | - Maybe even change color interpolation to homebrewed blending based on stackoverflow answer. This would need rgb instead of hex so might require the Color() function to return the colour object with possibility to make into any of rgb, hex, hsv, etc. Reason, some might think HSV interpolation is prettier but it is also less intuitive depending on which color range is being used (eg blue to red will pass through green and yellow), pluss no reason to force it, user can still just add yellow or whatever else color in between if they want: http://stackoverflow.com/questions/10901085/python-range-values-to-pseudocolor 25 | - Also function to add gridlines, should be simple 26 | - How about making the shapefile object more powerful so give it a selection method, a save method, and a split by method. Also possibility to create an empty shapefile by not specifying a shapefilepath, and then possible to build by using either shapely objects or manual coordinate lists. 27 | - Add philosophy section to README (about syntax policy and reasons behind it eg arcpy legacy) 28 | - Add simple zoomextent, by testing each shape for bbox in extent before rendering 29 | - Add Labels function with variable size/color/etc 30 | 31 | LONG TERM: 32 | 33 | - Enable partial transparency when rendering things... 34 | - Add numpy way to GetCenter in PyShpShape 35 | - Add possibility to set a renderorder based on a classification (similar to setting a symboltype like fillsize), and make a faster indexed way of rendering in sorted order (maybe by building my own indexfile of byte position of each shape). This bc sorted order rendering is slow bc grabs one shape at a time which is slower, as opposed to iterShapes(). 36 | - At some point maybe add ability to save classifications and layer symbols to pickle file, and potential for loading later on. 37 | 38 | - Find way to determine whether / or \ is correct path splitter 39 | - Add support for viewing shapefiles with the PyCairo renderer. 40 | - Add some more example scripts? 41 | - Add simple support for Python 3 42 | 43 | - Add single symbol classification 44 | - Add ability to zoom in to a certain extent instead of showing the entire world. 45 | - Add multiprocessing support (e.g. dividing up list of features to be rendered and then merging all partially rendered maps) 46 | - Add support for projected shapefiles and to change coordinate system/projection of your map. 47 | 48 | ABANDONED: 49 | 50 | /- Major question: force classifier to only one algorithm by specifying at startup, or allow potentially creative but chaotic and pointless multiple algorithms for each symbol part. 51 | /--- step1: classifytype, nrclasses, and excludevalues have to be moved from classification dict and made into Classifier self.object properties 52 | /--- step2: Classes should be moved back to a single property instead of an entry in each classification dict. 53 | /--- step3: AddClassification renamed to AddSymbolRule (also self.allclassifications, etc) 54 | /--- step4: SymbolClass must be made to store entire options dictionary, not just a single classsymbol property. 55 | -------------------------------------------------------------------------------- /developers/history_changelog.txt: -------------------------------------------------------------------------------- 1 | Version 0.2.0 (April 15, 2014): 2 | 3 | - ADD TO DOCS ABOUT PLAYING WITH SYMBOLS, AND THE AVAILABLE SYMBOL OPTIONS AND VALUES 4 | - ADD TO DOCS A PHILOSOPHY/SYNTAX SECTION 5 | - Add textsize factor option for AddLegend 6 | - Change license type both in script, and in README 7 | - ADD TO DOCS THAT IN PYCAIRO DRAWLINE IS ALWAYS BLACK, WEIRD ERROR, THUS PYCAIRO IS NOT FULLY SUPPORTED FOR X FEATURES...??? WHICH ONES? "PyCairo resulted in several errors that made it difficult to make compatible with all the other renderers, so support for it has been temporarily discontinued. All of the previous functions should work, but the new ones cannot be guaranteed." 8 | - Force mapdimensions to always be half eachothers size, so not to distort the map with screendims or manual user input 9 | - Add docstrings for all new features (REMAINING: AddLegend, AddText, DrawRectangle, DrawCircle, DrawLine, all the AskX functions, and the new excludequery and symbolizer and classifier options in newmap.AddToMap) 10 | - Reformat all docstrings to markdown format, so can easily be looped and written directly to a markdown documentation text file. 11 | - Add simple way to render text, default font, and place it to relative xy location 12 | - Add an easy way for the user to automatically classify and visualize individual shapefile features based on their underlying data values. User can use different classifications for different symbol parts of the shapefile (eg size based on some numbers field and categorical based on some country field). Each feature's value will be mapped to a user-specifed range of numbers or colorgradient (for the cologradient the user only specifies the colorstops/endpoints). 13 | - Add fisher jenks classification, and optimized for large datasets 14 | - Add categorical classification 15 | - Also fix error where nat breaks always uses 0 as lowest breakpoint instead of min value from list 16 | - Fix weird thing where self.classes only saves most recent classification, need to save classes of all classifications. SOLUTION: just pass the entire classification dictionary instead of specifying each argument, that way classes can be added to each classification dictionary and accessed later. While at it also sort out where the default classification stuff should be set(should be in AddClassification, bc _CalculateClasses should never really be used directly so should only receive the dictionary)... 17 | - Finally also make PIL renderer make 2x bigger image and then downsize/2 with Antialias filter to make it smoother. ALSO NOTE THAT IN PIL ALL SYMBOLSIZES ARE HALF AS SMALL BC MAP IS UPSCALED 2X, SO NEED TO TIMES ALL SYMBOL SIZES BY TWO UPON ENTERING 18 | - Add freeform rectangle/line/circle functions 19 | - Also add new point symbol types like pyramid and square 20 | - Enable invisible (None) when rendering symbol parts. WORKS, EXCEPT FOR LINES WHERE OUTLINE IS IN FACT A FILLED LINE BEHIND THE FILL LINE, Fix in future version 21 | - Make possible to specify background box when adding text 22 | - Possible to make, invert, and clear a selection on a shapefile so will only loop through those whose attributes evaluate to true according to the query. Query takes ordinary Python syntax and case sensitive fieldnames. 23 | - Add visual choice functions for colors, nrs, strings, and files for easier explorative/interactive usage (such as the AskColor function) 24 | - Add ability to exclude values when adding a shapefile to a newmap object, by asking for string expressions that can be used with an exec() function 25 | - Add outline around interactive map. 26 | - Find way to calculate symbolsizes as percentage of mapsize instead of absolute pixel value. SOLUTION: user gives relative fraction of screen, and then that gets timed with the screen to get pixel size! Only question is whether to do it relative to width or height (or maybe even area of screen covered by whichever symbol...) 27 | - Add symbolizer option when adding to map, which regardless of shapetype, determines what kind of graphics to be classified and used for symbolization, such as "circle","square","pyramid","picture","pyramidscape",etc. Default is basiccircle for points, basicline for lines(only available for lines), and polygon for polygons(only available for polygons). This is mostly used to force using those as symbols at centerpoints of polygon and line features, or for deciding which type of point symbol. Dont specify to use shapefile default. 28 | - Clean up/remove/move ColorGradient function to the listy strech function. 29 | - Add Legend function, it simply needs to loop thru each class of the classifier and use the class options dictionary to create an exact copy in the legend. 30 | - Gave the save map button an image icon instead of just text 31 | 32 | Version 0.1.0 (25.February 2014): 33 | 34 | - Basic functionality 35 | - One liners for shapefile viewing and map saving 36 | - Multiple layers custom map creation 37 | - Customize each shapefile with colors and fillsize -------------------------------------------------------------------------------- /examples/batch_add_to_map_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Script for the Python Geographic Visualizer (GeoVis) 3 | https://github.com/karimbahgat/geovis 4 | """ 5 | 6 | #importing geovis from temporary location 7 | TEMP_GEOVIS_FOLDER = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 8 | import sys 9 | sys.path.append(TEMP_GEOVIS_FOLDER) 10 | import geovis 11 | 12 | #setting up for speed 13 | geovis.SetRenderingOptions(reducevectors=True) 14 | 15 | #create map 16 | MAPCOLORSTYLE = "pastelle" 17 | geovis.SetMapBackground(geovis.Color("blue",style=MAPCOLORSTYLE)) 18 | newmap = geovis.NewMap() 19 | 20 | #add base shapefile layer 21 | base_layer = geovis.Layer("D:\Test Data\Global Subadmins\gadm2.shp") 22 | newmap.AddToMap(base_layer) 23 | 24 | #overlay with a folder of many shapefiles 25 | for eachfolder, eachfile, eachext in geovis.ShapefileFolder(r"D:\Test Data\DHS GPS"): 26 | eachlayer = geovis.Layer(filepath=eachfolder+eachfile+eachext, fillcolor=geovis.Color("random", style=MAPCOLORSTYLE)) 27 | newmap.AddToMap(eachlayer) 28 | 29 | #add title 30 | newmap.AddText(relx=0.5, rely=0.1, text="Batch Map Example", textsize=0.06) 31 | 32 | #finally view the map 33 | newmap.ViewMap() 34 | -------------------------------------------------------------------------------- /examples/pointmap_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Script for the Python Geographic Visualizer (GeoVis) 3 | https://github.com/karimbahgat/geovis 4 | """ 5 | 6 | #importing geovis from temporary location 7 | TEMP_GEOVIS_FOLDER = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 8 | import sys 9 | sys.path.append(TEMP_GEOVIS_FOLDER) 10 | import geovis 11 | 12 | ############ 13 | #SETUP 14 | ############ 15 | #set rendering options 16 | geovis.SetRenderingOptions(renderer="PIL", numpyspeed=False, reducevectors=False) 17 | #create map 18 | geovis.SetMapBackground(geovis.Color("blue", brightness=0.9)) 19 | geovis.SetMapZoom(x2x=[-120,40],y2y=[-60,20]) 20 | newmap = geovis.NewMap() 21 | 22 | ############ 23 | #LOAD AND SYMBOLIZE LAYERS 24 | ############ 25 | countrylayer = geovis.Layer(filepath=r"D:\Test Data\necountries\necountries.shp", fillcolor=geovis.Color("yellow",brightness=0.8)) 26 | pointlayer = geovis.Layer(filepath=r"D:\Test Data\GTD_Georef\gtd_georef.shp", symbolizer="square") 27 | pointlayer.AddClassification(symboltype="fillcolor", valuefield="nwound", symbolrange=[geovis.Color("white"),geovis.Color("red", intensity=0.9, brightness=0.9),geovis.Color("red", intensity=0.9, brightness=0.5)], classifytype="natural breaks", nrclasses=3) 28 | pointlayer.AddClassification(symboltype="fillsize", valuefield="nwound", symbolrange=[0.3,2.8], classifytype="natural breaks", nrclasses=3) 29 | 30 | ############ 31 | #RENDER TO MAP 32 | ############ 33 | #add layers to map 34 | newmap.AddToMap(countrylayer) 35 | newmap.AddToMap(pointlayer) 36 | #add legend 37 | newmap.AddLegend(pointlayer, upperleft=(0.5,0.7), bottomright=(0.9,0.9)) 38 | #view map 39 | newmap.ViewMap() 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/polygonmap_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Script for the Python Geographic Visualizer (GeoVis) 3 | https://github.com/karimbahgat/geovis 4 | """ 5 | 6 | #importing geovis from temporary location 7 | TEMP_GEOVIS_FOLDER = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 8 | import sys 9 | sys.path.append(TEMP_GEOVIS_FOLDER) 10 | import geovis 11 | 12 | ############ 13 | #SETUP 14 | ############ 15 | #setup rendering options 16 | geovis.SetRenderingOptions(renderer="PIL", numpyspeed=False, reducevectors=False) 17 | #create map 18 | geovis.SetMapBackground(geovis.Color("blue", brightness=0.9)) 19 | geovis.SetMapZoom(x2x=[-180,0],y2y=[-90,0]) 20 | newmap = geovis.NewMap() 21 | 22 | ############ 23 | #LOAD AND SYMBOLIZE LAYERS 24 | ############ 25 | countrylayer = geovis.Layer(filepath=r"D:\Test Data\necountries\necountries.shp") 26 | countrylayer.AddClassification(symboltype="fillcolor", valuefield="pop_est", symbolrange=[geovis.Color("white"),geovis.Color("red", intensity=0.9, brightness=0.8),geovis.Color("red", intensity=0.9, brightness=0.5)], classifytype="natural breaks", nrclasses=3) 27 | countrylayer.AddClassification(symboltype="outlinewidth", valuefield="pop_est", symbolrange=[0.05,0.4], classifytype="natural breaks", nrclasses=3) 28 | riverlayer = geovis.Layer(filepath=r"D:\Test Data\lines\ne_50m_rivers_lake_centerlines.shp", fillcolor=geovis.Color("blue",brightness=0.9)) 29 | 30 | ############ 31 | #RENDER TO MAP 32 | ############ 33 | #add layers to map 34 | newmap.AddToMap(countrylayer) 35 | newmap.AddToMap(riverlayer) 36 | #add legend 37 | newmap.AddLegend(countrylayer, upperleft=(0.03,0.15), bottomright=(0.6,0.4)) 38 | #view map 39 | newmap.ViewMap() 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/quickview_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Script for the Python Geographic Visualizer (GeoVis) 3 | https://github.com/karimbahgat/geovis 4 | """ 5 | 6 | #importing geovis from temporary location 7 | TEMP_GEOVIS_FOLDER = r"C:\Users\BIGKIMO\Documents\GitHub\geovis" 8 | import sys 9 | sys.path.append(TEMP_GEOVIS_FOLDER) 10 | import geovis 11 | 12 | #setting up for speed 13 | geovis.SetRenderingOptions(reducevectors=True) 14 | 15 | #view shapefile 16 | geovis.ViewShapefile(r"D:\Test Data\cshapes\cshapes.shp", maptitle="ViewShapefile Example") 17 | -------------------------------------------------------------------------------- /geovis/colour.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Color Library 3 | 4 | .. :doctest: 5 | 6 | This module defines several color formats that can be converted to one or 7 | another. 8 | 9 | Formats 10 | ------- 11 | 12 | HSL: 13 | 3-uple of Hue, Saturation, Value all between 0.0 and 1.0 14 | 15 | RGB: 16 | 3-uple of Red, Green, Blue all between 0.0 and 1.0 17 | 18 | HEX: 19 | string object beginning with '#' and with red, green, blue value. 20 | This format accept color in 3 or 6 value ex: '#fff' or '#ffffff' 21 | 22 | WEB: 23 | string object that defaults to HEX representation or human if possible 24 | 25 | Usage 26 | ----- 27 | 28 | Several function exists to convert from one format to another. But all 29 | function are not written. So the best way is to use the object Color. 30 | 31 | Please see the documentation of this object for more information. 32 | 33 | .. note:: Some constants are defined for convenience in HSL, RGB, HEX 34 | 35 | """ 36 | 37 | from __future__ import with_statement, print_function 38 | 39 | import hashlib 40 | import re 41 | 42 | ## 43 | ## Some Constants 44 | ## 45 | 46 | ## Soften inequalities and some rounding issue based on float 47 | FLOAT_ERROR = 0.0000005 48 | 49 | 50 | RGB_TO_COLOR_NAMES = { 51 | (0, 0, 0): ['Black'], 52 | (0, 0, 128): ['Navy', 'NavyBlue'], 53 | (0, 0, 139): ['DarkBlue'], 54 | (0, 0, 205): ['MediumBlue'], 55 | (0, 0, 255): ['Blue'], 56 | (0, 100, 0): ['DarkGreen'], 57 | (0, 128, 0): ['Green'], 58 | (0, 139, 139): ['DarkCyan'], 59 | (0, 191, 255): ['DeepSkyBlue'], 60 | (0, 206, 209): ['DarkTurquoise'], 61 | (0, 250, 154): ['MediumSpringGreen'], 62 | (0, 255, 0): ['Lime'], 63 | (0, 255, 127): ['SpringGreen'], 64 | (0, 255, 255): ['Cyan', 'Aqua'], 65 | (25, 25, 112): ['MidnightBlue'], 66 | (30, 144, 255): ['DodgerBlue'], 67 | (32, 178, 170): ['LightSeaGreen'], 68 | (34, 139, 34): ['ForestGreen'], 69 | (46, 139, 87): ['SeaGreen'], 70 | (47, 79, 79): ['DarkSlateGray', 'DarkSlateGrey'], 71 | (50, 205, 50): ['LimeGreen'], 72 | (60, 179, 113): ['MediumSeaGreen'], 73 | (64, 224, 208): ['Turquoise'], 74 | (65, 105, 225): ['RoyalBlue'], 75 | (70, 130, 180): ['SteelBlue'], 76 | (72, 61, 139): ['DarkSlateBlue'], 77 | (72, 209, 204): ['MediumTurquoise'], 78 | (75, 0, 130): ['Indigo'], 79 | (85, 107, 47): ['DarkOliveGreen'], 80 | (95, 158, 160): ['CadetBlue'], 81 | (100, 149, 237): ['CornflowerBlue'], 82 | (102, 205, 170): ['MediumAquamarine'], 83 | (105, 105, 105): ['DimGray', 'DimGrey'], 84 | (106, 90, 205): ['SlateBlue'], 85 | (107, 142, 35): ['OliveDrab'], 86 | (112, 128, 144): ['SlateGray', 'SlateGrey'], 87 | (119, 136, 153): ['LightSlateGray', 'LightSlateGrey'], 88 | (123, 104, 238): ['MediumSlateBlue'], 89 | (124, 252, 0): ['LawnGreen'], 90 | (127, 255, 0): ['Chartreuse'], 91 | (127, 255, 212): ['Aquamarine'], 92 | (128, 0, 0): ['Maroon'], 93 | (128, 0, 128): ['Purple'], 94 | (128, 128, 0): ['Olive'], 95 | (128, 128, 128): ['Gray', 'Grey'], 96 | (132, 112, 255): ['LightSlateBlue'], 97 | (135, 206, 235): ['SkyBlue'], 98 | (135, 206, 250): ['LightSkyBlue'], 99 | (138, 43, 226): ['BlueViolet'], 100 | (139, 0, 0): ['DarkRed'], 101 | (139, 0, 139): ['DarkMagenta'], 102 | (139, 69, 19): ['SaddleBrown'], 103 | (143, 188, 143): ['DarkSeaGreen'], 104 | (144, 238, 144): ['LightGreen'], 105 | (147, 112, 219): ['MediumPurple'], 106 | (148, 0, 211): ['DarkViolet'], 107 | (152, 251, 152): ['PaleGreen'], 108 | (153, 50, 204): ['DarkOrchid'], 109 | (154, 205, 50): ['YellowGreen'], 110 | (160, 82, 45): ['Sienna'], 111 | (165, 42, 42): ['Brown'], 112 | (169, 169, 169): ['DarkGray', 'DarkGrey'], 113 | (173, 216, 230): ['LightBlue'], 114 | (173, 255, 47): ['GreenYellow'], 115 | (175, 238, 238): ['PaleTurquoise'], 116 | (176, 196, 222): ['LightSteelBlue'], 117 | (176, 224, 230): ['PowderBlue'], 118 | (178, 34, 34): ['Firebrick'], 119 | (184, 134, 11): ['DarkGoldenrod'], 120 | (186, 85, 211): ['MediumOrchid'], 121 | (188, 143, 143): ['RosyBrown'], 122 | (189, 183, 107): ['DarkKhaki'], 123 | (192, 192, 192): ['Silver'], 124 | (199, 21, 133): ['MediumVioletRed'], 125 | (205, 92, 92): ['IndianRed'], 126 | (205, 133, 63): ['Peru'], 127 | (208, 32, 144): ['VioletRed'], 128 | (210, 105, 30): ['Chocolate'], 129 | (210, 180, 140): ['Tan'], 130 | (211, 211, 211): ['LightGray', 'LightGrey'], 131 | (216, 191, 216): ['Thistle'], 132 | (218, 112, 214): ['Orchid'], 133 | (218, 165, 32): ['Goldenrod'], 134 | (219, 112, 147): ['PaleVioletRed'], 135 | (220, 20, 60): ['Crimson'], 136 | (220, 220, 220): ['Gainsboro'], 137 | (221, 160, 221): ['Plum'], 138 | (222, 184, 135): ['Burlywood'], 139 | (224, 255, 255): ['LightCyan'], 140 | (230, 230, 250): ['Lavender'], 141 | (233, 150, 122): ['DarkSalmon'], 142 | (238, 130, 238): ['Violet'], 143 | (238, 221, 130): ['LightGoldenrod'], 144 | (238, 232, 170): ['PaleGoldenrod'], 145 | (240, 128, 128): ['LightCoral'], 146 | (240, 230, 140): ['Khaki'], 147 | (240, 248, 255): ['AliceBlue'], 148 | (240, 255, 240): ['Honeydew'], 149 | (240, 255, 255): ['Azure'], 150 | (244, 164, 96): ['SandyBrown'], 151 | (245, 222, 179): ['Wheat'], 152 | (245, 245, 220): ['Beige'], 153 | (245, 245, 245): ['WhiteSmoke'], 154 | (245, 255, 250): ['MintCream'], 155 | (248, 248, 255): ['GhostWhite'], 156 | (250, 128, 114): ['Salmon'], 157 | (250, 235, 215): ['AntiqueWhite'], 158 | (250, 240, 230): ['Linen'], 159 | (250, 250, 210): ['LightGoldenrodYellow'], 160 | (253, 245, 230): ['OldLace'], 161 | (255, 0, 0): ['Red'], 162 | (255, 0, 255): ['Magenta', 'Fuchsia'], 163 | (255, 20, 147): ['DeepPink'], 164 | (255, 69, 0): ['OrangeRed'], 165 | (255, 99, 71): ['Tomato'], 166 | (255, 105, 180): ['HotPink'], 167 | (255, 127, 80): ['Coral'], 168 | (255, 140, 0): ['DarkOrange'], 169 | (255, 160, 122): ['LightSalmon'], 170 | (255, 165, 0): ['Orange'], 171 | (255, 182, 193): ['LightPink'], 172 | (255, 192, 203): ['Pink'], 173 | (255, 215, 0): ['Gold'], 174 | (255, 218, 185): ['PeachPuff'], 175 | (255, 222, 173): ['NavajoWhite'], 176 | (255, 228, 181): ['Moccasin'], 177 | (255, 228, 196): ['Bisque'], 178 | (255, 228, 225): ['MistyRose'], 179 | (255, 235, 205): ['BlanchedAlmond'], 180 | (255, 239, 213): ['PapayaWhip'], 181 | (255, 240, 245): ['LavenderBlush'], 182 | (255, 245, 238): ['Seashell'], 183 | (255, 248, 220): ['Cornsilk'], 184 | (255, 250, 205): ['LemonChiffon'], 185 | (255, 250, 240): ['FloralWhite'], 186 | (255, 250, 250): ['Snow'], 187 | (255, 255, 0): ['Yellow'], 188 | (255, 255, 224): ['LightYellow'], 189 | (255, 255, 240): ['Ivory'], 190 | (255, 255, 255): ['White'] 191 | } 192 | 193 | ## Building inverse relation 194 | COLOR_NAME_TO_RGB = dict( 195 | (name.lower(), rgb) 196 | for rgb, names in RGB_TO_COLOR_NAMES.items() 197 | for name in names) 198 | 199 | 200 | LONG_HEX_COLOR = re.compile(r'^#[0-9a-fA-F]{6}$') 201 | SHORT_HEX_COLOR = re.compile(r'^#[0-9a-fA-F]{3}$') 202 | 203 | 204 | class HSL: 205 | BLACK = (0.0 , 0.0, 0.0) 206 | WHITE = (0.0 , 0.0, 1.0) 207 | RED = (0.0 , 1.0, 0.5) 208 | GREEN = (1.0/3, 1.0, 0.5) 209 | BLUE = (2.0/3, 1.0, 0.5) 210 | GRAY = (0.0 , 0.0, 0.5) 211 | 212 | 213 | class C_RGB: 214 | """RGB colors container 215 | 216 | Provides a quick color access. 217 | 218 | >>> from colour import RGB 219 | 220 | >>> RGB.WHITE 221 | (1.0, 1.0, 1.0) 222 | >>> RGB.BLUE 223 | (0.0, 0.0, 1.0) 224 | 225 | >>> RGB.DONOTEXISTS # doctest: +ELLIPSIS 226 | Traceback (most recent call last): 227 | ... 228 | AttributeError: ... has no attribute 'DONOTEXISTS' 229 | 230 | """ 231 | 232 | def __getattr__(self, value): 233 | return hsl2rgb(getattr(HSL, value)) 234 | 235 | 236 | class C_HEX: 237 | """RGB colors container 238 | 239 | Provides a quick color access. 240 | 241 | >>> from colour import HEX 242 | 243 | >>> HEX.WHITE 244 | '#fff' 245 | >>> HEX.BLUE 246 | '#00f' 247 | 248 | >>> HEX.DONOTEXISTS # doctest: +ELLIPSIS 249 | Traceback (most recent call last): 250 | ... 251 | AttributeError: ... has no attribute 'DONOTEXISTS' 252 | 253 | """ 254 | 255 | def __getattr__(self, value): 256 | return rgb2hex(getattr(RGB, value)) 257 | 258 | RGB = C_RGB() 259 | HEX = C_HEX() 260 | 261 | 262 | ## 263 | ## Convertion function 264 | ## 265 | 266 | def hsl2rgb(hsl): 267 | """Convert HSL representation towards RGB 268 | 269 | :param h: Hue, position around the chromatic circle (h=1 equiv h=0) 270 | :param s: Saturation, color saturation (0=full gray, 1=full color) 271 | :param l: Ligthness, Overhaul lightness (0=full black, 1=full white) 272 | :rtype: 3-uple for RGB values in float between 0 and 1 273 | 274 | Hue, Saturation, Range from Lightness is a float between 0 and 1 275 | 276 | Note that Hue can be set to any value but as it is a rotation 277 | around the chromatic circle, any value above 1 or below 0 can 278 | be expressed by a value between 0 and 1 (Note that h=0 is equiv 279 | to h=1). 280 | 281 | This algorithm came from: 282 | http://www.easyrgb.com/index.php?X=MATH&H=19#text19 283 | 284 | Here are some quick notion of HSL to RGB convertion: 285 | 286 | >>> from colour import hsl2rgb 287 | 288 | With a lightness put at 0, RGB is always rgbblack 289 | 290 | >>> hsl2rgb((0.0, 0.0, 0.0)) 291 | (0.0, 0.0, 0.0) 292 | >>> hsl2rgb((0.5, 0.0, 0.0)) 293 | (0.0, 0.0, 0.0) 294 | >>> hsl2rgb((0.5, 0.5, 0.0)) 295 | (0.0, 0.0, 0.0) 296 | 297 | Same for lightness put at 1, RGB is always rgbwhite 298 | 299 | >>> hsl2rgb((0.0, 0.0, 1.0)) 300 | (1.0, 1.0, 1.0) 301 | >>> hsl2rgb((0.5, 0.0, 1.0)) 302 | (1.0, 1.0, 1.0) 303 | >>> hsl2rgb((0.5, 0.5, 1.0)) 304 | (1.0, 1.0, 1.0) 305 | 306 | With saturation put at 0, the RGB should be equal to Lightness: 307 | 308 | >>> hsl2rgb((0.0, 0.0, 0.25)) 309 | (0.25, 0.25, 0.25) 310 | >>> hsl2rgb((0.5, 0.0, 0.5)) 311 | (0.5, 0.5, 0.5) 312 | >>> hsl2rgb((0.5, 0.0, 0.75)) 313 | (0.75, 0.75, 0.75) 314 | 315 | With saturation put at 1, and lightness put to 0.5, we can find 316 | normal full red, green, blue colors: 317 | 318 | >>> hsl2rgb((0 , 1.0, 0.5)) 319 | (1.0, 0.0, 0.0) 320 | >>> hsl2rgb((1 , 1.0, 0.5)) 321 | (1.0, 0.0, 0.0) 322 | >>> hsl2rgb((1.0/3 , 1.0, 0.5)) 323 | (0.0, 1.0, 0.0) 324 | >>> hsl2rgb((2.0/3 , 1.0, 0.5)) 325 | (0.0, 0.0, 1.0) 326 | 327 | Of course: 328 | >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS 329 | Traceback (most recent call last): 330 | ... 331 | ValueError: Saturation must be between 0 and 1. 332 | 333 | And: 334 | >>> hsl2rgb((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS 335 | Traceback (most recent call last): 336 | ... 337 | ValueError: Lightness must be between 0 and 1. 338 | 339 | """ 340 | h, s, l = [float(v) for v in hsl] 341 | 342 | if not (0.0 - FLOAT_ERROR <= s <= 1.0 + FLOAT_ERROR): 343 | raise ValueError("Saturation must be between 0 and 1.") 344 | if not (0.0 - FLOAT_ERROR <= l <= 1.0 + FLOAT_ERROR): 345 | raise ValueError("Lightness must be between 0 and 1.") 346 | 347 | if s == 0: 348 | return l, l, l 349 | 350 | if l < 0.5: 351 | v2 = l * (1.0 + s) 352 | else: 353 | v2 = (l + s) - (s * l) 354 | 355 | v1 = 2.0 * l - v2 356 | 357 | r = _hue2rgb(v1, v2, h + (1.0 / 3)) 358 | g = _hue2rgb(v1, v2, h) 359 | b = _hue2rgb(v1, v2, h - (1.0 / 3)) 360 | 361 | return r, g, b 362 | 363 | 364 | def rgb2hsl(rgb): 365 | """Convert RGB representation towards HSL 366 | 367 | :param r: Red amount (float between 0 and 1) 368 | :param g: Green amount (float between 0 and 1) 369 | :param b: Blue amount (float between 0 and 1) 370 | :rtype: 3-uple for HSL values in float between 0 and 1 371 | 372 | This algorithm came from: 373 | http://www.easyrgb.com/index.php?X=MATH&H=19#text19 374 | 375 | Here are some quick notion of RGB to HSL convertion: 376 | 377 | >>> from colour import rgb2hsl 378 | 379 | Note that if red amount is equal to green and blue, then you 380 | should have a gray value (from black to white). 381 | 382 | 383 | >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS 384 | (..., 0.0, 1.0) 385 | >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS 386 | (..., 0.0, 0.5) 387 | >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS 388 | (..., 0.0, 0.0) 389 | 390 | If only one color is different from the others, it defines the 391 | direct Hue: 392 | 393 | >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS 394 | (0.66..., 1.0, 0.75) 395 | >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS 396 | (0.0, 0.33..., 0.15...) 397 | 398 | Having only one value set, you can check that: 399 | 400 | >>> rgb2hsl((1.0, 0.0, 0.0)) 401 | (0.0, 1.0, 0.5) 402 | >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS 403 | (0.33..., 1.0, 0.5) 404 | >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS 405 | (0.66..., 1.0, 0.5) 406 | 407 | Of course: 408 | >>> rgb2hsl((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS 409 | Traceback (most recent call last): 410 | ... 411 | ValueError: Green must be between 0 and 1. You provided 2.0. 412 | 413 | And: 414 | >>> rgb2hsl((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS 415 | Traceback (most recent call last): 416 | ... 417 | ValueError: Blue must be between 0 and 1. You provided 1.5. 418 | 419 | """ 420 | r, g, b = [float(v) for v in rgb] 421 | 422 | for name, v in {'Red': r, 'Green': g, 'Blue': b}.items(): 423 | if not (0 - FLOAT_ERROR <= v <= 1 + FLOAT_ERROR): 424 | raise ValueError("%s must be between 0 and 1. You provided %r." 425 | % (name, v)) 426 | 427 | vmin = min(r, g, b) ## Min. value of RGB 428 | vmax = max(r, g, b) ## Max. value of RGB 429 | diff = vmax - vmin ## Delta RGB value 430 | 431 | vsum = vmin + vmax 432 | 433 | l = vsum / 2 434 | 435 | if diff == 0.0: ## This is a gray, no chroma... 436 | return (0.0, 0.0, l) 437 | 438 | ## 439 | ## Chromatic data... 440 | ## 441 | 442 | ## Saturation 443 | if l < 0.5: 444 | s = diff / vsum 445 | else: 446 | s = diff / (2.0 - vsum) 447 | 448 | dr = (((vmax - r) / 6) + (diff / 2)) / diff 449 | dg = (((vmax - g) / 6) + (diff / 2)) / diff 450 | db = (((vmax - b) / 6) + (diff / 2)) / diff 451 | 452 | if r == vmax: 453 | h = db - dg 454 | elif g == vmax: 455 | h = (1.0 / 3) + dr - db 456 | elif b == vmax: 457 | h = (2.0 / 3) + dg - dr 458 | 459 | if h < 0: h += 1 460 | if h > 1: h -= 1 461 | 462 | return (h, s, l) 463 | 464 | 465 | def _hue2rgb(v1, v2, vH): 466 | """Private helper function (Do not call directly) 467 | 468 | :param vH: rotation around the chromatic circle (between 0..1) 469 | 470 | """ 471 | 472 | while vH < 0: vH += 1 473 | while vH > 1: vH -= 1 474 | 475 | if 6 * vH < 1: return v1 + (v2 - v1) * 6 * vH 476 | if 2 * vH < 1: return v2 477 | if 3 * vH < 2: return v1 + (v2 - v1) * ((2.0 / 3) - vH) * 6 478 | 479 | return v1 480 | 481 | 482 | def rgb2hex(rgb, force_long=False): 483 | """Transform RGB tuple to hex RGB representation 484 | 485 | :param rgb: RGB 3-uple of float between 0 and 1 486 | :rtype: 3 hex char or 6 hex char string representation 487 | 488 | Usage 489 | ----- 490 | 491 | >>> from colour import rgb2hex 492 | 493 | >>> rgb2hex((0.0,1.0,0.0)) 494 | '#0f0' 495 | 496 | Rounding try to be as natural as possible: 497 | 498 | >>> rgb2hex((0.0,0.999999,1.0)) 499 | '#0ff' 500 | 501 | And if not possible, the 6 hex char representation is used: 502 | 503 | >>> rgb2hex((0.23,1.0,1.0)) 504 | '#3bffff' 505 | 506 | >>> rgb2hex((0.0,0.999999,1.0), force_long=True) 507 | '#00ffff' 508 | 509 | """ 510 | 511 | hx = '#' + ''.join(["%02x" % int(c*255 + 0.5 - FLOAT_ERROR) for c in rgb]) 512 | 513 | if force_long == False and \ 514 | hx[1] == hx[2] and \ 515 | hx[3] == hx[4] and \ 516 | hx[5] == hx[6]: 517 | return '#' + hx[1] + hx[3] + hx[5] 518 | 519 | return hx 520 | 521 | 522 | def hex2rgb(str_rgb): 523 | """Transform hex RGB representation to RGB tuple 524 | 525 | :param str_rgb: 3 hex char or 6 hex char string representation 526 | :rtype: RGB 3-uple of float between 0 and 1 527 | 528 | >>> from colour import hex2rgb 529 | 530 | >>> hex2rgb('#00ff00') 531 | (0.0, 1.0, 0.0) 532 | 533 | >>> hex2rgb('#0f0') 534 | (0.0, 1.0, 0.0) 535 | 536 | >>> hex2rgb('#aaa') # doctest: +ELLIPSIS 537 | (0.66..., 0.66..., 0.66...) 538 | 539 | >>> hex2rgb('#aa') # doctest: +ELLIPSIS 540 | Traceback (most recent call last): 541 | ... 542 | ValueError: Invalid value '#aa' provided for rgb color. 543 | 544 | """ 545 | 546 | try: 547 | rgb = str_rgb[1:] 548 | 549 | if len(rgb) == 6: 550 | r, g, b = rgb[0:2], rgb[2:4], rgb[4:6] 551 | elif len(rgb) == 3: 552 | r, g, b = rgb[0] * 2, rgb[1] * 2, rgb[2] * 2 553 | else: 554 | raise ValueError() 555 | except: 556 | raise ValueError("Invalid value %r provided for rgb color." 557 | % str_rgb) 558 | 559 | return tuple([float(int(v, 16)) / 255 for v in (r, g, b)]) 560 | 561 | 562 | def hex2web(hex): 563 | """Converts HEX representation to WEB 564 | 565 | :param rgb: 3 hex char or 6 hex char string representation 566 | :rtype: web string representation (human readable if possible) 567 | 568 | WEB representation uses X11 rgb.txt to define convertion 569 | between RGB and english color names. 570 | 571 | Usage 572 | ===== 573 | 574 | >>> from colour import hex2web 575 | 576 | >>> hex2web('#ff0000') 577 | 'red' 578 | 579 | >>> hex2web('#aaaaaa') 580 | '#aaa' 581 | 582 | >>> hex2web('#abc') 583 | '#abc' 584 | 585 | >>> hex2web('#acacac') 586 | '#acacac' 587 | 588 | """ 589 | dec_rgb = tuple(int(v * 255) for v in hex2rgb(hex)) 590 | if dec_rgb in RGB_TO_COLOR_NAMES: 591 | ## take the first one 592 | color_name = RGB_TO_COLOR_NAMES[dec_rgb][0] 593 | ## Enforce full lowercase for single worded color name. 594 | return color_name if len(re.sub(r"[^A-Z]", "", color_name)) > 1 \ 595 | else color_name.lower() 596 | 597 | # Hex format is verified by hex2rgb function. And should be 3 or 6 digit 598 | if len(hex) == 7: 599 | if hex[1] == hex[2] and \ 600 | hex[3] == hex[4] and \ 601 | hex[5] == hex[6]: 602 | return '#' + hex[1] + hex[3] + hex[5] 603 | return hex 604 | 605 | 606 | def web2hex(web, force_long=False): 607 | """Converts WEB representation to HEX 608 | 609 | :param rgb: web string representation (human readable if possible) 610 | :rtype: 3 hex char or 6 hex char string representation 611 | 612 | WEB representation uses X11 rgb.txt to define convertion 613 | between RGB and english color names. 614 | 615 | Usage 616 | ===== 617 | 618 | >>> from colour import web2hex 619 | 620 | >>> web2hex('red') 621 | '#f00' 622 | 623 | >>> web2hex('#aaa') 624 | '#aaa' 625 | 626 | >>> web2hex('#foo') # doctest: +ELLIPSIS 627 | Traceback (most recent call last): 628 | ... 629 | AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. 630 | 631 | >>> web2hex('#aaa', force_long=True) 632 | '#aaaaaa' 633 | 634 | >>> web2hex('#aaaaaa') 635 | '#aaaaaa' 636 | 637 | >>> web2hex('#aaaa') # doctest: +ELLIPSIS 638 | Traceback (most recent call last): 639 | ... 640 | AttributeError: '#aaaa' is not in web format. Need 3 or 6 hex digit. 641 | 642 | >>> web2hex('pinky') # doctest: +ELLIPSIS 643 | Traceback (most recent call last): 644 | ... 645 | ValueError: 'pinky' is not a recognized color. 646 | 647 | And color names are case insensitive: 648 | 649 | >>> Color('RED') 650 | 651 | 652 | """ 653 | if web.startswith('#'): 654 | if (LONG_HEX_COLOR.match(web) or 655 | (not force_long and SHORT_HEX_COLOR.match(web))): 656 | return web.lower() 657 | elif SHORT_HEX_COLOR.match(web) and force_long: 658 | return '#' + ''.join([("%s" % (t, )) * 2 for t in web[1:]]) 659 | raise AttributeError( 660 | "%r is not in web format. Need 3 or 6 hex digit." % web) 661 | 662 | web = web.lower() 663 | if web not in COLOR_NAME_TO_RGB: 664 | raise ValueError("%r is not a recognized color." % web) 665 | 666 | ## convert dec to hex: 667 | 668 | return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]], force_long) 669 | 670 | 671 | def color_scale(begin_hsl, end_hsl, nb): 672 | """Returns a list of nb color HSL tuples between begin_hsl and end_hsl 673 | 674 | >>> from colour import color_scale 675 | 676 | >>> [rgb2hex(hsl2rgb(hsl)) for hsl in color_scale((0, 1, 0.5), 677 | ... (1, 1, 0.5), 3)] 678 | ['#f00', '#0f0', '#00f', '#f00'] 679 | 680 | >>> [rgb2hex(hsl2rgb(hsl)) 681 | ... for hsl in color_scale((0, 0, 0), 682 | ... (0, 0, 1), 683 | ... 15)] # doctest: +ELLIPSIS 684 | ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] 685 | 686 | """ 687 | 688 | step = tuple([float(end_hsl[i] - begin_hsl[i]) / nb for i in range(0, 3)]) 689 | 690 | def mul(step, value): 691 | return tuple([v * value for v in step]) 692 | 693 | def add_v(step, step2): 694 | return tuple([v + step2[i] for i, v in enumerate(step)]) 695 | 696 | return [add_v(begin_hsl, mul(step, r)) for r in range(0, nb + 1)] 697 | 698 | 699 | ## 700 | ## Color Pickers 701 | ## 702 | 703 | def RGB_color_picker(obj): 704 | """Build a color representation from the string representation of an object 705 | 706 | This allows to quickly get a color from some data, with the 707 | additional benefit that the color will be the same as long as the 708 | (string representation of the) data is the same:: 709 | 710 | >>> from colour import RGB_color_picker, Color 711 | 712 | Same inputs produce the same result:: 713 | 714 | >>> RGB_color_picker("Something") == RGB_color_picker("Something") 715 | True 716 | 717 | ... but different inputs produce different colors:: 718 | 719 | >>> RGB_color_picker("Something") != RGB_color_picker("Something else") 720 | True 721 | 722 | In any case, we still get a ``Color`` object:: 723 | 724 | >>> isinstance(RGB_color_picker("Something"), Color) 725 | True 726 | 727 | """ 728 | 729 | ## Turn the input into a by 3-dividable string. SHA-384 is good because it 730 | ## divides into 3 components of the same size, which will be used to 731 | ## represent the RGB values of the color. 732 | digest = hashlib.sha384(str(obj).encode('utf-8')).hexdigest() 733 | 734 | ## Split the digest into 3 sub-strings of equivalent size. 735 | subsize = int(len(digest) / 3) 736 | splitted_digest = [digest[i * subsize: (i + 1) * subsize] 737 | for i in range(3)] 738 | 739 | ## Convert those hexadecimal sub-strings into integer and scale them down 740 | ## to the 0..1 range. 741 | max_value = float(int("f" * subsize, 16)) 742 | components = ( 743 | int(d, 16) ## Make a number from a list with hex digits 744 | / max_value ## Scale it down to [0.0, 1.0] 745 | for d in splitted_digest) 746 | 747 | return Color(rgb2hex(components)) ## Profit! 748 | 749 | 750 | def hash_or_str(obj): 751 | try: 752 | return hash((type(obj).__name__, obj)) 753 | except TypeError: 754 | ## Adds the type name to make sure two object of different type but 755 | ## identical string representation get distinguished. 756 | return type(obj).__name__ + str(obj) 757 | 758 | ## 759 | ## All purpose object 760 | ## 761 | 762 | class Color(object): 763 | """Abstraction of a color object 764 | 765 | Color object keeps information of a color. It can input/output to different 766 | format (HSL, RGB, HEX, WEB) and their partial representation. 767 | 768 | >>> from colour import Color, HSL 769 | 770 | >>> b = Color() 771 | >>> b.hsl = HSL.BLUE 772 | 773 | Access values 774 | ------------- 775 | 776 | >>> b.hue # doctest: +ELLIPSIS 777 | 0.66... 778 | >>> b.saturation 779 | 1.0 780 | >>> b.luminance 781 | 0.5 782 | 783 | >>> b.red 784 | 0.0 785 | >>> b.blue 786 | 1.0 787 | >>> b.green 788 | 0.0 789 | 790 | >>> b.rgb 791 | (0.0, 0.0, 1.0) 792 | >>> b.hsl # doctest: +ELLIPSIS 793 | (0.66..., 1.0, 0.5) 794 | >>> b.hex 795 | '#00f' 796 | 797 | Change values 798 | ------------- 799 | 800 | Let's change Hue toward red tint: 801 | 802 | >>> b.hue = 0.0 803 | >>> b.hex 804 | '#f00' 805 | 806 | >>> b.hue = 2.0/3 807 | >>> b.hex 808 | '#00f' 809 | 810 | In the other way round: 811 | 812 | >>> b.hex = '#f00' 813 | >>> b.hsl 814 | (0.0, 1.0, 0.5) 815 | 816 | Long hex can be accessed directly: 817 | 818 | >>> b.hex_l = '#123456' 819 | >>> b.hex_l 820 | '#123456' 821 | >>> b.hex 822 | '#123456' 823 | 824 | >>> b.hex_l = '#ff0000' 825 | >>> b.hex_l 826 | '#ff0000' 827 | >>> b.hex 828 | '#f00' 829 | 830 | Convenience 831 | ----------- 832 | 833 | >>> c = Color('blue') 834 | >>> c 835 | 836 | >>> c.hue = 0 837 | >>> c 838 | 839 | 840 | >>> c.saturation = 0.0 841 | >>> c.hsl # doctest: +ELLIPSIS 842 | (..., 0.0, 0.5) 843 | >>> c.rgb 844 | (0.5, 0.5, 0.5) 845 | >>> c.hex 846 | '#7f7f7f' 847 | >>> c 848 | 849 | 850 | >>> c.luminance = 0.0 851 | >>> c 852 | 853 | 854 | >>> c.hex 855 | '#000' 856 | 857 | >>> c.green = 1.0 858 | >>> c.blue = 1.0 859 | >>> c.hex 860 | '#0ff' 861 | >>> c 862 | 863 | 864 | >>> c = Color('blue', luminance=0.75) 865 | >>> c 866 | 867 | 868 | >>> c = Color('red', red=0.5) 869 | >>> c 870 | 871 | 872 | >>> print(c) 873 | #7f0000 874 | 875 | You can try to query unexisting attributes: 876 | 877 | >>> c.lightness # doctest: +ELLIPSIS 878 | Traceback (most recent call last): 879 | ... 880 | AttributeError: 'lightness' not found 881 | 882 | TODO: could add HSV, CMYK, YUV conversion. 883 | 884 | # >>> b.hsv 885 | # >>> b.value 886 | # >>> b.cyan 887 | # >>> b.magenta 888 | # >>> b.yellow 889 | # >>> b.key 890 | # >>> b.cmyk 891 | 892 | 893 | Recursive init 894 | -------------- 895 | 896 | To support blind convertion of web strings (or already converted object), 897 | the Color object supports instantiation with another Color object. 898 | 899 | >>> Color(Color(Color('red'))) 900 | 901 | 902 | Equality support 903 | ---------------- 904 | 905 | Default equality is RGB hex comparison: 906 | 907 | >>> Color('red') == Color('blue') 908 | False 909 | 910 | But this can be changed: 911 | 912 | >>> saturation_equality = lambda c1, c2: c1.luminance == c2.luminance 913 | >>> Color('red', equality=saturation_equality) == Color('blue') 914 | True 915 | 916 | """ 917 | 918 | _hsl = None ## internal representation 919 | 920 | def __init__(self, color=None, 921 | pick_for=None, picker=RGB_color_picker, pick_key=hash_or_str, 922 | **kwargs): 923 | 924 | if pick_key is None: 925 | pick_key = lambda x: x 926 | 927 | if pick_for is not None: 928 | color = picker(pick_key(pick_for)) 929 | 930 | if isinstance(color, Color): 931 | self.web = color.web 932 | else: 933 | self.web = color if color else 'black' 934 | 935 | self.equality = RGB_equivalence 936 | 937 | for k, v in kwargs.items(): 938 | setattr(self, k, v) 939 | 940 | def __getattr__(self, label): 941 | if ('get_' + label) in self.__class__.__dict__: 942 | return getattr(self, 'get_' + label)() 943 | raise AttributeError("'%s' not found" % label) 944 | 945 | def __setattr__(self, label, value): 946 | if label not in ["_hsl", "equality"]: 947 | fc = getattr(self, 'set_' + label) 948 | fc(value) 949 | else: 950 | self.__dict__[label] = value 951 | 952 | ## 953 | ## Get 954 | ## 955 | 956 | def get_hsl(self): 957 | return tuple(self._hsl) 958 | 959 | def get_hex(self): 960 | return rgb2hex(self.rgb) 961 | 962 | def get_hex_l(self): 963 | return rgb2hex(self.rgb, force_long=True) 964 | 965 | def get_rgb(self): 966 | return hsl2rgb(self.hsl) 967 | 968 | def get_hue(self): 969 | return self.hsl[0] 970 | 971 | def get_saturation(self): 972 | return self.hsl[1] 973 | 974 | def get_luminance(self): 975 | return self.hsl[2] 976 | 977 | def get_red(self): 978 | return self.rgb[0] 979 | 980 | def get_green(self): 981 | return self.rgb[1] 982 | 983 | def get_blue(self): 984 | return self.rgb[2] 985 | 986 | def get_web(self): 987 | return hex2web(self.hex) 988 | 989 | ## 990 | ## Set 991 | ## 992 | 993 | def set_hsl(self, value): 994 | self._hsl = list(value) 995 | 996 | def set_rgb(self, value): 997 | self.hsl = rgb2hsl(value) 998 | 999 | def set_hue(self, value): 1000 | self._hsl[0] = value 1001 | 1002 | def set_saturation(self, value): 1003 | self._hsl[1] = value 1004 | 1005 | def set_luminance(self, value): 1006 | self._hsl[2] = value 1007 | 1008 | def set_red(self, value): 1009 | r, g, b = self.rgb 1010 | r = value 1011 | self.rgb = (r, g, b) 1012 | 1013 | def set_green(self, value): 1014 | r, g, b = self.rgb 1015 | g = value 1016 | self.rgb = (r, g, b) 1017 | 1018 | def set_blue(self, value): 1019 | r, g, b = self.rgb 1020 | b = value 1021 | self.rgb = (r, g, b) 1022 | 1023 | def set_hex(self, value): 1024 | self.rgb = hex2rgb(value) 1025 | 1026 | set_hex_l = set_hex 1027 | 1028 | def set_web(self, value): 1029 | self.hex = web2hex(value) 1030 | 1031 | ## range of color generation 1032 | 1033 | def range_to(self, value, steps): 1034 | for hsl in color_scale(self._hsl, Color(value).hsl, steps - 1): 1035 | yield Color(hsl=hsl) 1036 | 1037 | ## 1038 | ## Convenience 1039 | ## 1040 | 1041 | def __str__(self): 1042 | return "%s" % self.web 1043 | 1044 | def __repr__(self): 1045 | return "" % self.web 1046 | 1047 | def __eq__(self, other): 1048 | return self.equality(self, other) 1049 | 1050 | 1051 | RGB_equivalence = lambda c1, c2: c1.hex_l == c2.hex_l 1052 | HSL_equivalence = lambda c1, c2: c1._hsl == c2._hsl 1053 | 1054 | 1055 | def make_color_factory(**kwargs_defaults): 1056 | 1057 | def ColorFactory(*args, **kwargs): 1058 | new_kwargs = kwargs_defaults.copy() 1059 | new_kwargs.update(kwargs) 1060 | return Color(*args, **new_kwargs) 1061 | return ColorFactory 1062 | -------------------------------------------------------------------------------- /geovis/guihelper.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | almost good 4 | but not sure how useful all of this is 5 | gets buggy/leans to one direction when using excessive padding isntead of just disappearing 6 | and seems that same can be done using relx/rely and nested widgets...? 7 | maybe the charm is to not use the subpartition (only if you really cant do nesting) 8 | but instead call PartitionSpace simply as an aid when doing relx/rely inside a nested widget 9 | """ 10 | 11 | class Partition: 12 | def __init__(self, center, partitionwidth, partitionheight, direction): 13 | if direction == "vertical": 14 | center = (center[1],center[0]) 15 | partitionwidth,partitionheight = partitionheight,partitionwidth 16 | self.center = center 17 | self.width = partitionwidth 18 | self.height = partitionheight 19 | midx,midy = center 20 | halfx = partitionwidth/2.0 21 | halfy = partitionheight/2.0 22 | self.nw = (midx-halfx, midy-halfy) 23 | self.n = (midx, midy-halfy) 24 | self.ne = (midx+halfx, midy-halfy) 25 | self.e = (midx+halfx, midy) 26 | self.se = (midx+halfx, midy+halfy) 27 | self.s = (midx, midy+halfy) 28 | self.sw = (midx-halfx, midy+halfy) 29 | self.w = (midx-halfx, midy) 30 | def __repr__(self): 31 | visualstring = "partition:\n" 32 | visualstring += "%s \t %s \t %s \n" % tuple([[str(nr)[:5] for nr in pair] for pair in (self.nw,self.n,self.ne)]) 33 | visualstring += "%s \t %s \t %s \n" % tuple([[str(nr)[:5] for nr in pair] for pair in (self.w,self.center,self.e)]) 34 | visualstring += "%s \t %s \t %s" % tuple([[str(nr)[:5] for nr in pair] for pair in (self.sw,self.s,self.se)]) 35 | return visualstring 36 | def SubPartition(self, partitions, padx, pady, direction="horizontal"): 37 | "only use if you really need to avoid nested widgets, eg if you need to structure your widgets over a large background image bc a background widget to nest them in would cover up parts of the image" 38 | xtox = (self.w[0], self.e[0]) 39 | ytoy = (self.n[1], self.s[1]) 40 | return PartitionSpace(xtox, ytoy, partitions, padx, pady, direction) 41 | 42 | def PartitionSpace(xtox, ytoy, partitions, padx, pady, direction="horizontal"): 43 | startx, endx = xtox 44 | starty, endy = ytoy 45 | if direction == "vertical": 46 | startx,starty = starty,startx 47 | endx,endy = endy,endx 48 | padx,pady = pady,padx 49 | #prep 50 | allwidth = endx-startx 51 | allheight = endy-starty 52 | widthafterpad = allwidth-padx*(partitions+1) 53 | heightafterpad = allheight-pady*2 54 | partitionwidth = widthafterpad/float(partitions) 55 | partitionheight = heightafterpad 56 | #calc 57 | outpartitions = [] 58 | tempx = startx+padx+partitionwidth/2.0 59 | tempy = starty+pady+partitionheight/2.0 60 | for _ in xrange(partitions): 61 | center = (tempx, tempy) 62 | outpartitions.append( Partition(center, partitionwidth, partitionheight, direction) ) 63 | tempx += partitionwidth/2.0+padx+partitionwidth/2.0 64 | return outpartitions 65 | 66 | if __name__ == "__main__": 67 | import Tkinter as tk 68 | import random 69 | win = tk.Tk() 70 | testdims = (1100,500) 71 | testparts = 3 72 | 73 | #pixel positions test 74 | ## testpadx,testpady = (90,20) 75 | ## frame = tk.Frame(win, bg="red", width=testdims[0], height=testdims[1]) 76 | ## frame.pack() 77 | ## hm = tk.Label(frame, bg="yellow") 78 | ## hm.place(x=200,y=200) 79 | ## for partition in PartitionSpace(200,testdims[0],0,testdims[1],testparts,testpadx,testpady): 80 | ## but = tk.Label(frame, bg="blue") 81 | ## x,y = partition.center 82 | ## print partition.w, x, partition.e 83 | ## but.place(x=x, y=y, width=round(partition.width), height=round(partition.height), anchor="center") 84 | 85 | #rel test 86 | testpadx,testpady = (0.01,0.01) 87 | frame = tk.Frame(win, bg="red", width=testdims[0], height=testdims[1]) 88 | frame.pack(fill="both") 89 | hm = tk.Label(frame, bg="yellow") 90 | hm.place(relx=0.3,rely=0.5, anchor="ne") 91 | #horiz partitions 92 | partitions = PartitionSpace((0.3,1),(0.7,1),testparts,testpadx,testpady) 93 | subpart = partitions[0] 94 | for partition in partitions: 95 | but = tk.Label(frame, bg="blue") 96 | x,y = partition.center 97 | but.place(relx=x, rely=y, relwidth=partition.width, relheight=partition.height, anchor="center") 98 | #split each down the middle 99 | subpartitions = partition.SubPartition(2, 0.01, 0.02) 100 | #left side 101 | subpart = subpartitions[0] 102 | but = tk.Label(frame, bg="green") 103 | x,y = subpart.center 104 | but.place(relx=x, rely=y, relwidth=subpart.width, relheight=subpart.height, anchor="center") 105 | #then try downwards partition 106 | subsubpartitions = subpart.SubPartition(5, 0.006, 0.006, direction="vertical") 107 | for subpart in subsubpartitions: 108 | but = tk.Label(frame, bg="red") 109 | x,y = subpart.center 110 | but.place(relx=x, rely=y, relwidth=subpart.width, relheight=subpart.height, anchor="center") 111 | #right side 112 | subpart = subpartitions[1] 113 | but = tk.Label(frame, bg="green") 114 | x,y = subpart.center 115 | but.place(relx=x, rely=y, relwidth=subpart.width, relheight=subpart.height, anchor="center") 116 | #then try downwards partition 117 | subsubpartitions = subpart.SubPartition(5, 0.006, 0.006, direction="vertical") 118 | for subpart in subsubpartitions: 119 | but = tk.Label(frame, bg="white", text="text") 120 | x,y = subpart.center 121 | but.place(relx=x, rely=y, relwidth=subpart.width, relheight=subpart.height, anchor="center") 122 | #finalize window 123 | coordsdisplay = tk.Label(win, text="mousepos") 124 | coordsdisplay.place(relx=0.01, rely=0.01) 125 | def displaymousecoords(event): 126 | coordsdisplay["text"] = "%s,%s (%s,%s)" %(event.x,event.y, event.x/float(win.winfo_width()),event.y/float(win.winfo_height())) 127 | win.bind("", displaymousecoords) 128 | win.mainloop() 129 | 130 | -------------------------------------------------------------------------------- /geovis/listy.py: -------------------------------------------------------------------------------- 1 | #IMPORTS 2 | import sys, os, operator, itertools, random, math 3 | 4 | #GLOBALS 5 | NONEVALUE = None 6 | 7 | 8 | """ 9 | SEE ALSO 10 | http://code.activestate.com/recipes/189971-basic-linear-algebra-matrix/ 11 | http://users.rcn.com/python/download/python.htm 12 | """ 13 | 14 | 15 | #FUNCTIONS 16 | def Resize(rows, newlength, stretchmethod="not specified", gapvalue="not specified"): 17 | "front end used by user, determines if special nested list resizing or single list" 18 | if isinstance(rows[0], (list,tuple)): 19 | #input list is a sequence of list (but only the first item is checked) 20 | #needs to have same nr of list items in all sublists 21 | crosssection = itertools.izip(*rows) 22 | grad_crosssection = [ _Resize(spectrum,newlength,stretchmethod,gapvalue) for spectrum in crosssection ] 23 | gradient = [list(each) for each in itertools.izip(*grad_crosssection)] 24 | return gradient 25 | else: 26 | #just a single list of values 27 | return _Resize(rows, newlength, stretchmethod, gapvalue) 28 | def Transpose(listoflists): 29 | "must get a 2d grid, ie a list of lists, with all sublists having equal lengths" 30 | transposed = [list(each) for each in itertools.izip(*listoflists)] 31 | return transposed 32 | 33 | #BUILTINS 34 | def _Resize(rows, newlength, stretchmethod="not specified", gapvalue="not specified"): 35 | "behind the scenes, does the actual work, only for a single flat list" 36 | #return input as is if no difference in length 37 | if newlength == len(rows): 38 | return rows 39 | #set gap 40 | if gapvalue == "not specified": 41 | gapvalue = NONEVALUE 42 | #set auto stretchmode 43 | if stretchmethod == "not specified": 44 | if isinstance(rows[0], (int,float)): 45 | stretchmethod = "interpolate" 46 | else: 47 | stretchmethod = "duplicate" 48 | #reduce newlength 49 | newlength -= 1 50 | #assign first value 51 | outlist = [rows[0]] 52 | relspreadindexgen = (index/float(len(rows)-1) for index in xrange(1,len(rows))) #warning a little hacky by skipping first index cus is assigned auto 53 | relspreadindex = next(relspreadindexgen) 54 | spreadflag = False 55 | for each in xrange(1, newlength): 56 | #relative positions 57 | rel = each/float(newlength) 58 | relindex = (len(rows)-1) * rel 59 | basenr,decimals = str(relindex).split(".") 60 | relbwindex = float("0."+decimals) 61 | #determine equivalent value 62 | if stretchmethod=="interpolate": 63 | maybecurrelval = rows[int(relindex)] 64 | if maybecurrelval != gapvalue: 65 | #ALMOST THERE BUT NOT QUITE 66 | #DOES SOME WEIRD BACKTRACKING 67 | #... 68 | currelval = rows[int(relindex)] 69 | #make sure next value to interpolate to is valid 70 | testindex = int(relindex)+1 71 | while testindex < len(rows)-1 and rows[testindex] == gapvalue: 72 | #if not then interpolate until next valid item 73 | testindex += 1 74 | nextrelval = rows[testindex] 75 | #assign value 76 | relbwval = currelval + (nextrelval - currelval) * relbwindex #basenr pluss interindex percent interpolation of diff to next item 77 | elif stretchmethod=="duplicate": 78 | relbwval = rows[int(round(relindex))] #no interpolation possible, so just copy each time 79 | elif stretchmethod=="spread": 80 | if rel >= relspreadindex: 81 | spreadindex = int(len(rows)*relspreadindex) 82 | relbwval = rows[spreadindex] #spread values further apart so as to leave gaps in between 83 | relspreadindex = next(relspreadindexgen) 84 | else: 85 | relbwval = gapvalue 86 | #assign each value 87 | outlist.append(relbwval) 88 | #assign last value 89 | outlist.append(rows[-1]) 90 | return outlist 91 | def _InterpolateValue(value, otherinput, method): 92 | "method can be linear, IDW, etc..." 93 | pass 94 | 95 | #CLASSES 96 | class _1dData: 97 | """ 98 | Most basic of all list types. Contains data values but is just a meaningless arbitrary list if not embedded in some other list type. 99 | It can be embedded in a 2dsurfacegrid to represent a theme located along horizantal x lines. 100 | Or embedded in a 4dtimegrid to represent a theme changing over time, without any spatial properties. 101 | Maybe same as Listy below?? 102 | """ 103 | pass 104 | class _Cell: 105 | def __init__(self, xpos, ypos, value): 106 | self.x = xpos 107 | self.y = ypos 108 | self.value = value 109 | class _2dSurfaceGrid: 110 | "horizontal lines up and down along y axis" 111 | #BUILTINS 112 | def __init__(self, twoDlist=None, emptydims="not specified"): 113 | #add some error checking... 114 | #... 115 | if not twoDlist: 116 | if emptydims == "not specified": 117 | emptydims = (50,50) 118 | width,height = emptydims 119 | twoDlist = [[NONEVALUE for _ in xrange(width)] for _ in xrange(height)] 120 | self.grid = Listy(*twoDlist) 121 | self.height = len(self.grid.lists) 122 | self.width = len(self.grid.lists[0]) 123 | self.knowncells = self._GetKnownCells() 124 | def __iter__(self): 125 | for ypos, horizline in enumerate(self.grid.lists): 126 | for xpos, xpoint in enumerate(horizline): 127 | yield _Cell(xpos, ypos, value=xpoint) 128 | def __str__(self): 129 | return str(self.grid) 130 | #FUNCTIONS 131 | def RandomPoints(self, value="random", valuerange="not specified", nrpoints="not specified"): 132 | if nrpoints == "not specified": 133 | nrpoints = int(self.width*self.height*0.10) #10 percent of all cells 134 | if valuerange == "not specified": 135 | valuerange = (0,250) 136 | randomvalue = False 137 | if value == "random": 138 | randomvalue = True 139 | for _ in xrange(nrpoints): 140 | if randomvalue: 141 | value = random.randrange(*valuerange) 142 | xindex = random.randrange(self.width) 143 | yindex = random.randrange(self.height) 144 | self.grid.lists[yindex][xindex] = value 145 | self.knowncells = self._GetKnownCells() 146 | def Interpolate(self, method="IDW", **options): 147 | "ie fill any gaps in grid with interpolation" 148 | if method == "IDW": 149 | self._IDW(options) 150 | def ChangeValues(self, expression): 151 | pass 152 | def SelectQuery(self, query): 153 | pass 154 | def Show(self): 155 | import numpy, PIL, PIL.Image, PIL.ImageTk, PIL.ImageDraw 156 | import Tkinter as tk 157 | import colour 158 | win = tk.Tk() 159 | nparr = numpy.array(self.grid.lists) 160 | npmin = numpy.min(nparr) 161 | npmax = numpy.max(nparr) 162 | minmaxdiff = npmax-npmin 163 | colorstops = [colour.Color("red").rgb,colour.Color("yellow").rgb,colour.Color("green").rgb] 164 | colorstops = [list(each) for each in colorstops] 165 | colorgrad = Listy(*colorstops) 166 | colorgrad.Convert("250*value") 167 | colorgrad.Resize(int(minmaxdiff)) 168 | valuerange = range(int(npmin),int(npmax)) 169 | colordict = dict(zip(valuerange,colorgrad.lists)) 170 | print len(valuerange),len(colorgrad.lists),len(colordict) 171 | print "minmax",npmin,npmax 172 | for ypos,horizline in enumerate(self.grid.lists): 173 | for xpos,value in enumerate(horizline): 174 | relval = value/float(npmax) 175 | self.grid.lists[ypos][xpos] = colorgrad.lists[int((len(colorgrad.lists)-1)*relval)] 176 | nparr = numpy.array(self.grid.lists,"uint8") 177 | print "np shape",nparr.shape 178 | img = PIL.Image.fromarray(nparr) 179 | drawer = PIL.ImageDraw.ImageDraw(img) 180 | size = 3 181 | for knowncell in self.knowncells: 182 | x,y = (knowncell.x,knowncell.y) 183 | drawer.ellipse((x-size,y-size,x+size,y+size),fill="black") 184 | img.save("C:/Users/BIGKIMO/Desktop/test.png") 185 | tkimg = PIL.ImageTk.PhotoImage(img) 186 | lbl = tk.Label(win, image=tkimg) 187 | lbl.pack() 188 | win.mainloop() 189 | #INTERNAL USE ONLY 190 | def _GetKnownCells(self): 191 | knowncellslist = [] 192 | for cell in self: 193 | if cell.value != NONEVALUE: 194 | knowncellslist.append(cell) 195 | return knowncellslist 196 | def _IDW(self, options): 197 | #retrieve input options 198 | neighbours = options.get("neighbours",int(len(self.knowncells)*0.10)) #default neighbours is 10 percent of known points 199 | sensitivity = options.get("sensitivity",3) #same as power, ie that high sensitivity means much more effect from far away points 200 | #some defs 201 | def _calcvalue(unknowncell, knowncells): 202 | weighted_values_sum = 0.0 203 | sum_of_weights = 0.0 204 | for knowncell in knowncells: 205 | weight = ((unknowncell.x-knowncell.x)**2 + (unknowncell.y-knowncell.y)**2)**(-sensitivity/2.0) 206 | sum_of_weights += weight 207 | weighted_values_sum += weight * knowncell.value 208 | return weighted_values_sum / sum_of_weights 209 | #calculate value 210 | for unknowncell in self: 211 | if unknowncell.value == NONEVALUE: 212 | #only calculate for unknown points 213 | self.grid.lists[unknowncell.y][unknowncell.x] = _calcvalue(unknowncell, self.knowncells) 214 | 215 | class _3dSpaceGrid: 216 | """z axis. 217 | Works by rendering one surface at a time, starting with lowest, that way rendering higher up/closer to the eye points on top of lower/further away points which gives a 3d effect. 218 | Just need to find a way to transform each surface to the way it should look like from different angles. 219 | Note: Link to a function to create the transform coeffs for PIL's perspective transform: http://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil 220 | OR use ray tracing... http://scratchapixel.com/lessons/3d-basic-lessons/lesson-1-writing-a-simple-raytracer/source-code/ 221 | ALSO see basic 3d equations http://www.math.washington.edu/~king/coursedir/m445w04/notes/vector/equations.html 222 | """ 223 | pass 224 | class _4dTimeGrid: 225 | """time axis 226 | example: 227 | for 3dtime in 4dtimegrid: 228 | #loops all 3d spaces at different points in time 229 | for 2ddepth in 3dtime: 230 | #loops all 2d surfaces at different altitudes, from low to high 231 | for 1dheight in 2ddepth: 232 | #loops all leftright horizantal lines in a 2dgrid 233 | for datavalue in 1dheight: 234 | #loops all datavalues for whatever theme in a specific 1dline, in a 2dgrid, at a 3daltitude, at a 4d point in time 235 | """ 236 | pass 237 | 238 | class Listy(list): 239 | "A list-type class with extended functionality and methods. These extra features should only be bindings to functions alredy defined in the general listy module and can be used by anyone, not just the Listy class." 240 | def __init__(self, *sequences): 241 | self.lists = list(sequences) 242 | self.dtype = "numbers" #"maybe do some automatic dtype detection" 243 | def __str__(self): 244 | maxchar = 50 245 | printstr = "[\n" 246 | for eachlist in self.lists: 247 | if len(str(eachlist)) > maxchar: 248 | printstr += str(eachlist)[:int(maxchar/2.0)]+"..."+str(eachlist)[-int(maxchar/2.0):]+"\n" 249 | else: 250 | printstr += str(eachlist)+"\n" 251 | printstr += "]\n" 252 | return printstr 253 | #SHAPE AND ORIENTATION 254 | def Resize(self, newlength, listdim=None, stretchmethod="not specified", gapvalue="not specified"): 255 | if listdim: 256 | #resize only a certain dimension 257 | self.lists[listdim] = Resize(self.lists[listdim], newlength, stretchmethod, gapvalue) 258 | else: 259 | #resize all lists??? experimental... 260 | self.lists = Resize(self.lists, newlength, stretchmethod, gapvalue) 261 | def Transpose(self): 262 | self.lists = Transpose(self.lists) 263 | def Reshape(self, newshape): 264 | "not sure yet how to do..." 265 | pass 266 | def Split(self, groups_or_indexes): 267 | pass 268 | #TYPES 269 | def Convert(self, dataformat): 270 | "it does actually work...but floating values will never be shortened bc no exact precision" 271 | toplist = self.lists 272 | def execfunc(code, toplist, value, index): 273 | exec(code) 274 | def recurloop(toplist): 275 | for index, value in enumerate(toplist): 276 | if isinstance(value, list): 277 | listfound = True 278 | recurloop(value) 279 | else: 280 | conversioncode = "toplist[index] = "+dataformat 281 | execfunc(conversioncode, toplist, value, index) 282 | #begin 283 | recurloop(toplist) 284 | #ATTRIBUTES 285 | @property 286 | def minval(self): 287 | pass 288 | @property 289 | def maxval(self): 290 | pass 291 | @property 292 | def shape(self): 293 | return tuple([len(eachlist) for eachlist in self.lists]) 294 | @property 295 | def structure(self): 296 | toplist = self.lists 297 | depth = 0 298 | spaces = " " 299 | structstring = str(len(toplist))+"\n" 300 | def recurloop(toplist, structstring, depth, spaces): 301 | for item in toplist: 302 | if isinstance(item, list): 303 | listfound = True 304 | depth += 1 305 | structstring += spaces*depth + str(len(item)) + "\n" 306 | toplist, structstring, depth, spaces = recurloop(item, structstring, depth, spaces) 307 | depth -= 1 308 | return toplist, structstring, depth, spaces 309 | #begin 310 | item, structstring, depth, spaces = recurloop(toplist, structstring, depth, spaces) 311 | return structstring 312 | 313 | if __name__ == "__main__": 314 | 315 | print "" 316 | print "print and shape and resize test" 317 | testlist = [random.randrange(50) for e in xrange(random.randrange(31))] 318 | testlisty = Listy(testlist, testlist) 319 | print testlisty 320 | print testlisty.shape 321 | testlisty.Resize(22, listdim=0) 322 | print testlisty.shape 323 | print testlisty 324 | 325 | print "" 326 | print "hierarchical nested list structure test" 327 | nestedlists = [[["anything" for e in xrange(random.randrange(500))] for e in xrange(random.randrange(5))] for d in xrange(random.randrange(6))] 328 | nestedlisty = Listy(*nestedlists) 329 | print nestedlisty 330 | print nestedlisty.structure 331 | 332 | print "" 333 | print "transpose test" 334 | listoflists = [range(100) for _ in xrange(100)] 335 | gridlisty = Listy(*listoflists) 336 | print gridlisty 337 | gridlisty.Transpose() 338 | print gridlisty 339 | 340 | print "" 341 | print "fill in blanks test" 342 | listholes = [1,2,None,4,None,6] 343 | gridlisty = Listy(*listholes) 344 | print gridlisty 345 | gridlisty.Resize(12, stretchmethod="interpolate") 346 | print gridlisty 347 | 348 | print "" 349 | print "spread grid test" 350 | listoflists = [range(6) for _ in xrange(6)] 351 | gridlisty = Listy(*listoflists) 352 | print gridlisty 353 | #expand sideways (instead of downwards which is default for the multilist resize func) 354 | gridlisty.Transpose() 355 | gridlisty.Resize(11, stretchmethod="spread") 356 | gridlisty.Transpose() 357 | #resize again to also expand downwards 358 | gridlisty.Resize(11, stretchmethod="spread") 359 | print gridlisty 360 | #finally try interpolating in between 361 | #THIS PART NOT WORKING PROPERLY, GOING ZIGZAG OVER GAPS 362 | ## gridlisty.Resize(22, stretchmethod="interpolate") 363 | ## print gridlisty 364 | ## gridlisty.Resize(11) 365 | ## print gridlisty 366 | 367 | print "" 368 | print "gridpoints interpolate test" 369 | listoflists = [[random.randrange(200) for _ in xrange(10)] for _ in xrange(10)] #[range(10) for _ in xrange(10)] 370 | templisty = Listy(*listoflists) 371 | print templisty 372 | #spread to create holes 373 | templisty.Transpose() 374 | templisty.Resize(80, stretchmethod="spread") 375 | templisty.Transpose() 376 | templisty.Resize(80, stretchmethod="spread") 377 | #make into 2dgrid obj 378 | testgrid = _2dSurfaceGrid(templisty.lists) 379 | print "spread done"#,testgrid 380 | #interpolate 381 | testgrid.Interpolate("IDW") 382 | print "idw done"#,testgrid.grid.lists 383 | #reduce decimals 384 | ##testgrid.grid.Convert("str(round(value,2))") 385 | ##print testgrid 386 | #testgrid.Show() 387 | 388 | print "" 389 | print "randompoints interpolate test" 390 | #create empty 2dgrid obj 391 | testgrid = _2dSurfaceGrid(emptydims=(600,600)) 392 | print "grid made"#,testgrid 393 | #put random points 394 | testgrid.RandomPoints(nrpoints=20) 395 | print "points placed"#,testgrid 396 | #interpolate 397 | import time 398 | t=time.clock() 399 | testgrid.Interpolate("IDW", sensitivity=4) 400 | print time.clock()-t 401 | print "idw done"#,testgrid.grid.lists 402 | testgrid.Show() 403 | 404 | 405 | -------------------------------------------------------------------------------- /geovis/messages.py: -------------------------------------------------------------------------------- 1 | # Import main modules 2 | import sys, pickle, Queue 3 | # Import custom modules 4 | from textual import txt 5 | import timetaker as timer 6 | 7 | def SnapVars(onlyvars=[], excludevars=[]): 8 | """ 9 | not working yet bc only snaps local vars... 10 | """ 11 | print ("snapshot of variables:") 12 | print ("----------------------") 13 | if onlyvars: allvars = onlyvars 14 | else: allvars = dir() 15 | for var in allvars: 16 | if not var.startswith("__"): 17 | if var in excludevars: 18 | continue 19 | val = eval(var) 20 | print ("%s = %s"%(var,val)) 21 | 22 | def Report(text, conditional=True): 23 | if isinstance(text, list): 24 | text = [txt(eachitem) for eachitem in text] 25 | text = ", ".join(text) 26 | if conditional == True: 27 | print text 28 | 29 | class ProgressReport: 30 | """generator that is wrapped over an iterator in a for-loop (like enumerate) 31 | and yields the same output as the iterator, except it also automatically checks 32 | and reports back the progress of the loop. Does not currently work for generators 33 | bc no way to assess length of a generator, unless it's a custom made one that has 34 | the __len__ attribute or if you specify the generator length manually. NOTE: using this wrapper function does not significantly reduce 35 | speed as long as you use it on loops that are large enough to make the difference neglible, 36 | that is, try not to use it on small loops; although it makes a loop 5x slower. For 37 | every 1 million loops this only amounts to a time punishment of about 0.6 seconds. 38 | Also, it is not very accurate for small time amounts, and can only measure times larger 39 | than 0.00005 seconds. 40 | *reportincr = report progress every x percent 41 | *genlength = if the iterator is a generator the iterator length has to be set manually in this variable""" 42 | def __init__(self, iterable, **kwargs): 43 | self.kwargs = kwargs 44 | self.iterable = iterable 45 | self.prog = 0 46 | def __iter__(self): 47 | iterable = self.iterable 48 | if not "shellreport" in self.kwargs: 49 | shellreport="progressbar" 50 | else: 51 | shellreport=self.kwargs["shellreport"] 52 | if not "text" in self.kwargs: 53 | text="unknown task" 54 | else: 55 | text=self.kwargs["text"] 56 | if not "tkwidget" in self.kwargs: 57 | tkwidget=None 58 | else: 59 | tkwidget=self.kwargs["tkwidget"] 60 | if not "queue" in self.kwargs: 61 | queue=None 62 | else: 63 | queue=self.kwargs["queue"] 64 | if not "picklepath" in self.kwargs: 65 | picklepath=None 66 | else: 67 | picklepath=self.kwargs["picklepath"] 68 | if not "reportincr" in self.kwargs: 69 | reportincr=1 70 | else: 71 | reportincr=self.kwargs["reportincr"] 72 | if not "genlength" in self.kwargs: 73 | genlength=None 74 | else: 75 | genlength=self.kwargs["genlength"] 76 | if not "countmethod" in self.kwargs: 77 | countmethod="auto" 78 | else: 79 | countmethod=self.kwargs["countmethod"] 80 | #some error checking 81 | if not hasattr(iterable, "__iter__"): 82 | raise TypeError("The iterable argument was not iterable") 83 | if not hasattr(iterable, "__len__") and not genlength: 84 | raise TypeError("The iterable argument must have a length in order to asses its progress") 85 | #determine report types 86 | if not shellreport: 87 | shellprogbar=False 88 | shellprint=False 89 | elif shellreport.lower() == "progressbar": 90 | shellprogbar=True 91 | shellprint=False 92 | elif shellreport.lower() == "print": 93 | shellprogbar=False 94 | shellprint=True 95 | #do some startup things 96 | if shellprogbar: 97 | reportincr = 2 98 | print "\n%s" %text 99 | print "0%"+","*50+"100%" 100 | sys.stdout.write(" ") 101 | #convert report incr percent to fraction of one 102 | reportincr = reportincr/100.0 103 | #measure total length 104 | if not genlength: 105 | total = float(len(iterable)) 106 | else: 107 | total = float(genlength) 108 | nextthresh = reportincr 109 | self.prog = 0 110 | timer.start("task completed in") 111 | for index, each in enumerate(iterable): 112 | if countmethod == "auto": 113 | #only if countmethod is set to "auto" will progress increase automatically 114 | #otherwise, the user has to keep a reference to the ProgressReport obj 115 | #and manually increment self.prog at the correct pace 116 | self.prog = index 117 | percent = self.prog/total 118 | #report progress if reached threshold 119 | if percent >= nextthresh: 120 | nextthresh += reportincr 121 | #if progressbar is true this will ignore the shellprint option 122 | if shellprogbar: 123 | sys.stdout.write("|") 124 | elif shellprint: 125 | print "%i percent task completion: %s" %(int(percent*100),text) 126 | if queue: 127 | queue.put({"percent":int(percent*100),"text":text}) 128 | if tkwidget: 129 | #tkwidget.set(int(percent*100)) 130 | tkwidget.update() 131 | if picklepath: 132 | msgbox = open(picklepath,"wb") 133 | pickle.dump({"percent":int(percent*100),"text":text}, msgbox) 134 | msgbox.close() 135 | #check for finish 136 | if nextthresh >= 1: 137 | if shellprogbar: 138 | sys.stdout.write("\n"+" "*8) 139 | timer.stop("task completed in") 140 | sys.stdout.write("\n") 141 | elif shellprint: 142 | print "%i percent task completion: %s" %(100,text) 143 | if queue: 144 | queue.put({"percent":100,"text":text}) 145 | if tkwidget: 146 | #tkwidget.set(int(percent*100)) 147 | tkwidget.update() 148 | if picklepath: 149 | msgbox = open(picklepath,"wb") 150 | pickle.dump({"percent":100,"text":text}, msgbox) 151 | msgbox.close() 152 | #yield next element from iterable 153 | yield each 154 | def Increment(self): 155 | self.prog += 1 156 | 157 | 158 | # example testing 159 | if __name__ == "__main__": 160 | import timetaker as timer 161 | l = xrange(1000000) 162 | timer.start() 163 | for each in ProgressReport(l): 164 | pass 165 | timer.stop() 166 | print "done" 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /geovis/shapefile_fork.py: -------------------------------------------------------------------------------- 1 | """ 2 | shapefile.py 3 | Provides read and write support for ESRI Shapefiles. 4 | author: jlawheadgeospatialpython.com 5 | date: 20130727 6 | version: 1.1.9 7 | Compatible with Python versions 2.4-3.x 8 | """ 9 | __version__ = "1.1.9" 10 | 11 | from struct import pack, unpack, calcsize, error 12 | import os 13 | import sys 14 | import time 15 | import array 16 | import tempfile 17 | import itertools 18 | try: 19 | import numpy 20 | except: 21 | pass 22 | 23 | # 24 | # Constants for shape types 25 | NULL = 0 26 | POINT = 1 27 | POLYLINE = 3 28 | POLYGON = 5 29 | MULTIPOINT = 8 30 | POINTZ = 11 31 | POLYLINEZ = 13 32 | POLYGONZ = 15 33 | MULTIPOINTZ = 18 34 | POINTM = 21 35 | POLYLINEM = 23 36 | POLYGONM = 25 37 | MULTIPOINTM = 28 38 | MULTIPATCH = 31 39 | 40 | GEOJ_TO_SHAPETYPE = {\ 41 | "Null":NULL, 42 | "Point":POINT, 43 | "LineString":POLYLINE, 44 | "Polygon":POLYGON, 45 | "MultiPoint":MULTIPOINT, 46 | "MultiLineString":POLYLINE, 47 | "MultiPolygon":POLYGON} 48 | 49 | PYTHON3 = sys.version_info[0] == 3 50 | 51 | if PYTHON3: 52 | xrange = range 53 | 54 | def b(v): 55 | if PYTHON3: 56 | if isinstance(v, str): 57 | # For python 3 encode str to bytes. 58 | return v.encode('utf-8') 59 | elif isinstance(v, bytes): 60 | # Already bytes. 61 | return v 62 | else: 63 | # Error. 64 | raise Exception('Unknown input type') 65 | else: 66 | # For python 2 assume str passed in and return str. 67 | return v 68 | 69 | def u(v): 70 | if PYTHON3: 71 | if isinstance(v, bytes): 72 | # For python 3 decode bytes to str. 73 | return v.decode('utf-8') 74 | elif isinstance(v, str): 75 | # Already str. 76 | return v 77 | else: 78 | # Error. 79 | raise Exception('Unknown input type') 80 | else: 81 | # For python 2 assume str passed in and return str. 82 | return v 83 | 84 | def is_string(v): 85 | if PYTHON3: 86 | return isinstance(v, str) 87 | else: 88 | return isinstance(v, basestring) 89 | 90 | def is_dict(v): 91 | return isinstance(v, dict) 92 | 93 | def geojson_to_pyshp(geoj): 94 | """Creates and returns a shape object based on information from a geojson geometry dictionary""" 95 | record = _Shape() 96 | # See if a geojson dictionary was passed as an argument 97 | if is_dict(geoj): 98 | record.shapeType = GEOJ_TO_SHAPETYPE[geoj["type"]] 99 | #record.bbox = [] 100 | if geoj["type"] == "Point": 101 | record.points = geoj["coordinates"] 102 | record.parts = [0] 103 | elif geoj["type"] in ("MultiPoint","Linestring"): 104 | record.points = geoj["coordinates"] 105 | record.parts = [0] 106 | elif geoj["type"] in ("Polygon"): 107 | record.points = geoj["coordinates"][0] 108 | record.parts = [0] 109 | elif geoj["type"] in ("MultiPolygon","MultiLineString"): 110 | index = 0 111 | points = [] 112 | parts = [] 113 | for eachmulti in geoj["coordinates"]: 114 | points.extend(eachmulti[0]) 115 | parts.append(index) 116 | index += len(eachmulti[0]) 117 | record.points = points 118 | record.parts = parts 119 | return record 120 | 121 | class _Array(array.array): 122 | """Converts python tuples to lits of the appropritate type. 123 | Used to unpack different shapefile header parts.""" 124 | def __repr__(self): 125 | return str(self.tolist()) 126 | 127 | def signed_area(coords): 128 | """Return the signed area enclosed by a ring using the linear time 129 | algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 130 | indicates a counter-clockwise oriented ring. 131 | """ 132 | xs, ys = map(list, zip(*coords)) 133 | xs.append(xs[1]) 134 | ys.append(ys[1]) 135 | return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in xrange(1, len(coords)))/2.0 136 | 137 | class _Shape: 138 | def __init__(self, shapeType=None): 139 | """Stores the geometry of the different shape types 140 | specified in the Shapefile spec. Shape types are 141 | usually point, polyline, or polygons. Every shape type 142 | except the "Null" type contains points at some level for 143 | example verticies in a polygon. If a shape type has 144 | multiple shapes containing points within a single 145 | geometry record then those shapes are called parts. Parts 146 | are designated by their starting index in geometry record's 147 | list of shapes.""" 148 | self.shapeType = shapeType 149 | self.points = [] 150 | 151 | @property 152 | def __geo_interface__(self): 153 | if self.shapeType in [POINT, POINTM, POINTZ]: 154 | return { 155 | 'type': 'Point', 156 | 'coordinates': tuple(self.points[0]) 157 | } 158 | elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]: 159 | return { 160 | 'type': 'MultiPoint', 161 | 'coordinates': tuple([tuple(p) for p in self.points]) 162 | } 163 | elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]: 164 | if len(self.parts) == 1: 165 | return { 166 | 'type': 'LineString', 167 | 'coordinates': tuple([tuple(p) for p in self.points]) 168 | } 169 | else: 170 | ps = None 171 | coordinates = [] 172 | for part in self.parts: 173 | if ps == None: 174 | ps = part 175 | continue 176 | else: 177 | coordinates.append(tuple([tuple(p) for p in self.points[ps:part]])) 178 | ps = part 179 | else: 180 | coordinates.append(tuple([tuple(p) for p in self.points[part:]])) 181 | return { 182 | 'type': 'MultiLineString', 183 | 'coordinates': tuple(coordinates) 184 | } 185 | elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: 186 | if len(self.parts) == 1: 187 | return { 188 | 'type': 'Polygon', 189 | 'coordinates': (tuple([tuple(p) for p in self.points]),) 190 | } 191 | else: 192 | ps = None 193 | coordinates = [] 194 | for part in self.parts: 195 | if ps == None: 196 | ps = part 197 | continue 198 | else: 199 | coordinates.append(tuple([tuple(p) for p in self.points[ps:part]])) 200 | ps = part 201 | else: 202 | coordinates.append(tuple([tuple(p) for p in self.points[part:]])) 203 | polys = [] 204 | poly = [coordinates[0]] 205 | for coord in coordinates[1:]: 206 | if signed_area(coord) < 0: 207 | polys.append(poly) 208 | poly = [coord] 209 | else: 210 | poly.append(coord) 211 | polys.append(poly) 212 | if len(polys) == 1: 213 | return { 214 | 'type': 'Polygon', 215 | 'coordinates': tuple(polys[0]) 216 | } 217 | elif len(polys) > 1: 218 | return { 219 | 'type': 'MultiPolygon', 220 | 'coordinates': polys 221 | } 222 | 223 | class _ShapeRecord: 224 | """A shape object of any type.""" 225 | def __init__(self, shape=None, record=None): 226 | self.shape = shape 227 | self.record = record 228 | 229 | class ShapefileException(Exception): 230 | """An exception to handle shapefile specific problems.""" 231 | pass 232 | 233 | class Reader: 234 | """Reads the three files of a shapefile as a unit or 235 | separately. If one of the three files (.shp, .shx, 236 | .dbf) is missing no exception is thrown until you try 237 | to call a method that depends on that particular file. 238 | The .shx index file is used if available for efficiency 239 | but is not required to read the geometry from the .shp 240 | file. The "shapefile" argument in the constructor is the 241 | name of the file you want to open. 242 | 243 | You can instantiate a Reader without specifying a shapefile 244 | and then specify one later with the load() method. 245 | 246 | Only the shapefile headers are read upon loading. Content 247 | within each file is only accessed when required and as 248 | efficiently as possible. Shapefiles are usually not large 249 | but they can be. 250 | """ 251 | def __init__(self, *args, **kwargs): 252 | self.shp = None 253 | self.shx = None 254 | self.dbf = None 255 | self.shapeName = "Not specified" 256 | self._offsets = [] 257 | self.shpLength = None 258 | self.numRecords = None 259 | self.fields = [] 260 | self.__dbfHdrLength = 0 261 | # See if a shapefile name was passed as an argument 262 | if len(args) > 0: 263 | if is_string(args[0]): 264 | self.load(args[0]) 265 | return 266 | if "shp" in kwargs.keys(): 267 | if hasattr(kwargs["shp"], "read"): 268 | self.shp = kwargs["shp"] 269 | if hasattr(self.shp, "seek"): 270 | self.shp.seek(0) 271 | if "shx" in kwargs.keys(): 272 | if hasattr(kwargs["shx"], "read"): 273 | self.shx = kwargs["shx"] 274 | if hasattr(self.shx, "seek"): 275 | self.shx.seek(0) 276 | if "dbf" in kwargs.keys(): 277 | if hasattr(kwargs["dbf"], "read"): 278 | self.dbf = kwargs["dbf"] 279 | if hasattr(self.dbf, "seek"): 280 | self.dbf.seek(0) 281 | if self.shp or self.dbf: 282 | self.load() 283 | else: 284 | raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") 285 | 286 | def __len__(self): 287 | return self.numRecords 288 | 289 | def load(self, shapefile=None): 290 | """Opens a shapefile from a filename or file-like 291 | object. Normally this method would be called by the 292 | constructor with the file object or file name as an 293 | argument.""" 294 | if shapefile: 295 | (shapeName, ext) = os.path.splitext(shapefile) 296 | self.shapeName = shapeName 297 | try: 298 | self.shp = open("%s.shp" % shapeName, "rb") 299 | except IOError: 300 | raise ShapefileException("Unable to open %s.shp" % shapeName) 301 | try: 302 | self.shx = open("%s.shx" % shapeName, "rb") 303 | except IOError: 304 | raise ShapefileException("Unable to open %s.shx" % shapeName) 305 | try: 306 | self.dbf = open("%s.dbf" % shapeName, "rb") 307 | except IOError: 308 | raise ShapefileException("Unable to open %s.dbf" % shapeName) 309 | if self.shp: 310 | self.__shpHeader() 311 | if self.dbf: 312 | self.__dbfHeader() 313 | 314 | def __getFileObj(self, f): 315 | """Checks to see if the requested shapefile file object is 316 | available. If not a ShapefileException is raised.""" 317 | if not f: 318 | raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") 319 | if self.shp and self.shpLength is None: 320 | self.load() 321 | if self.dbf and len(self.fields) == 0: 322 | self.load() 323 | return f 324 | 325 | def __restrictIndex(self, i): 326 | """Provides list-like handling of a record index with a clearer 327 | error message if the index is out of bounds.""" 328 | if self.numRecords: 329 | rmax = self.numRecords - 1 330 | if abs(i) > rmax: 331 | raise IndexError("Shape or Record index out of range.") 332 | if i < 0: i = range(self.numRecords)[i] 333 | return i 334 | 335 | def __shpHeader(self): 336 | """Reads the header information from a .shp or .shx file.""" 337 | if not self.shp: 338 | raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found") 339 | shp = self.shp 340 | # File length (16-bit word * 2 = bytes) 341 | shp.seek(24) 342 | self.shpLength = unpack(">i", shp.read(4))[0] * 2 343 | # Shape type 344 | shp.seek(32) 345 | self.shapeType= unpack("2i", fread(8)) 361 | # Determine the start of the next record 362 | next = f.tell() + (2 * recLength) 363 | shapeType = unpack(" 1: 382 | record.parts = record.parts[0] 383 | else: 384 | record.parts = unpack("<%si" % nParts, fread(nParts * 4)) 385 | # Read part types for Multipatch - 31 386 | if shapeType == 31: 387 | record.partTypes = _Array('i', unpack("<%si" % nParts, fread(nParts * 4))) 388 | # Read points - produces a list of [x,y] values 389 | if nPoints: 390 | if numpyspeed: 391 | record.points = numpy.fromfile(f, numpy.dtype('<2d'), nPoints) 392 | else: 393 | record.points = [unpack("<2d", fread(16)) for p in xrange(nPoints)] 394 | # Read z extremes and values 395 | if shapeType in (13,15,18,31): 396 | (zmin, zmax) = unpack("<2d", fread(16)) 397 | record.z = _Array('d', unpack("<%sd" % nPoints, fread(nPoints * 8))) 398 | # Read m extremes and values if header m values do not equal 0.0 399 | if shapeType in (13,15,18,23,25,28,31) and not 0.0 in self.measure: 400 | (mmin, mmax) = unpack("<2d", fread(16)) 401 | # Measure values less than -10e38 are nodata values according to the spec 402 | record.m = [] 403 | for m in _Array('d', unpack("<%sd" % nPoints, fread(nPoints * 8))): 404 | if m > -10e38: 405 | record.m.append(m) 406 | else: 407 | record.m.append(None) 408 | # Read a single point 409 | if shapeType in (1,11,21): 410 | record.points = [_Array('d', unpack("<2d", fread(16)))] 411 | # Read a single Z value 412 | if shapeType == 11: 413 | record.z = unpack("i", shx.read(4))[0] * 2) - 100 433 | numRecords = shxRecordLength // 8 434 | # Jump to the first record. 435 | shx.seek(100) 436 | for r in xrange(numRecords): 437 | # Offsets are 16-bit words just like the file length 438 | self._offsets.append(unpack(">i", shx.read(4))[0] * 2) 439 | shx.seek(shx.tell() + 4) 440 | if not i == None: 441 | return self._offsets[i] 442 | 443 | def shape(self, i=0, numpyspeed=False): 444 | """Returns a shape object for a shape in the the geometry 445 | record file. Numpyspeed can be set to True for faster shape 446 | retrieval (this has no effect if shx index file exists). 447 | Requires numpy.""" 448 | shp = self.__getFileObj(self.shp) 449 | i = self.__restrictIndex(i) 450 | offset = self.__shapeIndex(i) 451 | if not offset: 452 | # Shx index not available so iterate the full list. 453 | for j,k in enumerate(self.iterShapes(numpyspeed)): 454 | if j == i: 455 | return k 456 | shp.seek(offset) 457 | return self.__shape() 458 | 459 | def shapes(self): 460 | """Returns all shapes in a shapefile.""" 461 | shp = self.__getFileObj(self.shp) 462 | # Found shapefiles which report incorrect 463 | # shp file length in the header. Can't trust 464 | # that so we seek to the end of the file 465 | # and figure it out. 466 | shp.seek(0,2) 467 | self.shpLength = shp.tell() 468 | shp.seek(100) 469 | shapes = [] 470 | while shp.tell() < self.shpLength: 471 | shapes.append(self.__shape()) 472 | return shapes 473 | 474 | def iterShapes(self, numpyspeed=False): 475 | """Serves up shapes in a shapefile as an iterator. Useful 476 | for handling large shapefiles. The user has the option to set numpyspeed arg to True to allow for 477 | shapereading speed increase, especially for larger files. This requires numpy.""" 478 | shp = self.__getFileObj(self.shp) 479 | shp.seek(0,2) 480 | self.shpLength = shp.tell() 481 | shp.seek(100) 482 | while shp.tell() < self.shpLength: 483 | yield self.__shape(numpyspeed=numpyspeed) 484 | 485 | def __dbfHeaderLength(self): 486 | """Retrieves the header length of a dbf file header.""" 487 | if not self.__dbfHdrLength: 488 | if not self.dbf: 489 | raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)") 490 | dbf = self.dbf 491 | (self.numRecords, self.__dbfHdrLength) = \ 492 | unpack("6i", 9994,0,0,0,0,0)) 772 | # File length (Bytes / 2 = 16-bit words) 773 | if headerType == 'shp': 774 | f.write(pack(">i", self.__shpFileLength())) 775 | elif headerType == 'shx': 776 | f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2))) 777 | # Version, Shape type 778 | f.write(pack("<2i", 1000, self.shapeType)) 779 | # The shapefile's bounding box (lower left, upper right) 780 | if self.shapeType != 0: 781 | try: 782 | f.write(pack("<4d", *self.bbox())) 783 | except error: 784 | raise ShapefileException("Failed to write shapefile bounding box. Floats required.") 785 | else: 786 | f.write(pack("<4d", 0,0,0,0)) 787 | # Elevation 788 | z = self.zbox() 789 | # Measure 790 | m = self.mbox() 791 | try: 792 | f.write(pack("<4d", z[0], z[1], m[0], m[1])) 793 | except error: 794 | raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.") 795 | 796 | def __dbfHeader(self): 797 | """Writes the dbf header and field descriptors.""" 798 | f = self.__getFileObj(self.dbf) 799 | f.seek(0) 800 | version = 3 801 | year, month, day = time.localtime()[:3] 802 | year -= 1900 803 | # Remove deletion flag placeholder from fields 804 | for field in self.fields: 805 | if field[0].startswith("Deletion"): 806 | self.fields.remove(field) 807 | numRecs = len(self.records) 808 | numFields = len(self.fields) 809 | headerLength = numFields * 32 + 33 810 | recordLength = sum([int(field[2]) for field in self.fields]) + 1 811 | header = pack('2i", recNum, 0)) 836 | recNum += 1 837 | start = f.tell() 838 | # Shape Type 839 | if self.shapeType != 31: 840 | s.shapeType = self.shapeType 841 | f.write(pack("i", length)) 936 | f.seek(finish) 937 | 938 | def __shxRecords(self): 939 | """Writes the shx records.""" 940 | f = self.__getFileObj(self.shx) 941 | f.seek(100) 942 | for i in xrange(len(self._shapes)): 943 | f.write(pack(">i", self._offsets[i] // 2)) 944 | f.write(pack(">i", self._lengths[i])) 945 | 946 | def __dbfRecords(self): 947 | """Writes the dbf records.""" 948 | f = self.__getFileObj(self.dbf) 949 | for record in self.records: 950 | if not self.fields[0][0].startswith("Deletion"): 951 | f.write(b(' ')) # deletion flag 952 | for (fieldName, fieldType, size, dec), value in itertools.izip(self.fields, record): 953 | fieldType = fieldType.upper() 954 | size = int(size) 955 | if fieldType.upper() == "N": 956 | value = str(value).rjust(size) 957 | elif fieldType == 'L': 958 | value = str(value)[0].upper() 959 | else: 960 | value = str(value)[:size].ljust(size) 961 | assert len(value) == size 962 | value = b(value) 963 | f.write(value) 964 | 965 | def null(self): 966 | """Creates a null shape.""" 967 | self._shapes.append(_Shape(NULL)) 968 | 969 | def write_geoj(self, geoj): 970 | """Converts a geojson geometry dictionary and writes its shapefile equivalent""" 971 | converted_shape = geojson_to_pyshp(geoj) 972 | self._shapes.append(converted_shape) 973 | 974 | def point(self, x, y, z=0, m=0): 975 | """Creates a point shape.""" 976 | pointShape = _Shape(self.shapeType) 977 | pointShape.points.append([x, y, z, m]) 978 | self._shapes.append(pointShape) 979 | 980 | def line(self, parts=[], shapeType=POLYLINE): 981 | """Creates a line shape. This method is just a convienience method 982 | which wraps 'poly()'. 983 | """ 984 | self.poly(parts, shapeType, []) 985 | 986 | def poly(self, parts=[], shapeType=POLYGON, partTypes=[]): 987 | """Creates a shape that has multiple collections of points (parts) 988 | including lines, polygons, and even multipoint shapes. If no shape type 989 | is specified it defaults to 'polygon'. If no part types are specified 990 | (which they normally won't be) then all parts default to the shape type. 991 | """ 992 | polyShape = _Shape(shapeType) 993 | polyShape.parts = [] 994 | polyShape.points = [] 995 | # Make sure polygons are closed 996 | if shapeType in (5,15,25,31): 997 | for part in parts: 998 | if part[0] != part[-1]: 999 | part.append(part[0]) 1000 | for part in parts: 1001 | polyShape.parts.append(len(polyShape.points)) 1002 | for point in part: 1003 | # Ensure point is list 1004 | if not isinstance(point, list): 1005 | point = list(point) 1006 | # Make sure point has z and m values 1007 | while len(point) < 4: 1008 | point.append(0) 1009 | polyShape.points.append(point) 1010 | if polyShape.shapeType == 31: 1011 | if not partTypes: 1012 | for part in parts: 1013 | partTypes.append(polyShape.shapeType) 1014 | polyShape.partTypes = partTypes 1015 | self._shapes.append(polyShape) 1016 | 1017 | def field(self, name, fieldType="C", size="50", decimal=0): 1018 | """Adds a dbf field descriptor to the shapefile.""" 1019 | self.fields.append((name, fieldType, size, decimal)) 1020 | 1021 | def record(self, *recordList, **recordDict): 1022 | """Creates a dbf attribute record. You can submit either a sequence of 1023 | field values or keyword arguments of field names and values. Before 1024 | adding records you must add fields for the record values using the 1025 | fields() method. If the record values exceed the number of fields the 1026 | extra ones won't be added. In the case of using keyword arguments to specify 1027 | field/value pairs only fields matching the already registered fields 1028 | will be added.""" 1029 | record = [] 1030 | fieldCount = len(self.fields) 1031 | # Compensate for deletion flag 1032 | if self.fields[0][0].startswith("Deletion"): fieldCount -= 1 1033 | if recordList: 1034 | [record.append(recordList[i]) for i in xrange(fieldCount)] 1035 | elif recordDict: 1036 | for field in self.fields: 1037 | if field[0] in recordDict: 1038 | val = recordDict[field[0]] 1039 | if val is None: 1040 | record.append("") 1041 | else: 1042 | record.append(val) 1043 | if record: 1044 | self.records.append(record) 1045 | 1046 | def shape(self, i): 1047 | return self._shapes[i] 1048 | 1049 | def shapes(self): 1050 | """Return the current list of shapes.""" 1051 | return self._shapes 1052 | 1053 | def saveShp(self, target): 1054 | """Save an shp file.""" 1055 | if not hasattr(target, "write"): 1056 | target = os.path.splitext(target)[0] + '.shp' 1057 | if not self.shapeType: 1058 | self.shapeType = self._shapes[0].shapeType 1059 | self.shp = self.__getFileObj(target) 1060 | self.__shapefileHeader(self.shp, headerType='shp') 1061 | self.__shpRecords() 1062 | 1063 | def saveShx(self, target): 1064 | """Save an shx file.""" 1065 | if not hasattr(target, "write"): 1066 | target = os.path.splitext(target)[0] + '.shx' 1067 | if not self.shapeType: 1068 | self.shapeType = self._shapes[0].shapeType 1069 | self.shx = self.__getFileObj(target) 1070 | self.__shapefileHeader(self.shx, headerType='shx') 1071 | self.__shxRecords() 1072 | 1073 | def saveDbf(self, target): 1074 | """Save a dbf file.""" 1075 | if not hasattr(target, "write"): 1076 | target = os.path.splitext(target)[0] + '.dbf' 1077 | self.dbf = self.__getFileObj(target) 1078 | self.__dbfHeader() 1079 | self.__dbfRecords() 1080 | 1081 | def save(self, target=None, shp=None, shx=None, dbf=None): 1082 | """Save the shapefile data to three files or 1083 | three file-like objects. SHP and DBF files can also 1084 | be written exclusively using saveShp, saveShx, and saveDbf respectively. 1085 | If target is specified but not shp,shx, or dbf then the target path and 1086 | file name are used. If no options or specified, a unique base file name 1087 | is generated to save the files and the base file name is returned as a 1088 | string. 1089 | """ 1090 | # Create a unique file name if one is not defined 1091 | if shp: 1092 | self.saveShp(shp) 1093 | if shx: 1094 | self.saveShx(shx) 1095 | if dbf: 1096 | self.saveDbf(dbf) 1097 | elif not shp and not shx and not dbf: 1098 | generated = False 1099 | if not target: 1100 | temp = tempfile.NamedTemporaryFile(prefix="shapefile_",dir=os.getcwd()) 1101 | target = temp.name 1102 | generated = True 1103 | self.saveShp(target) 1104 | self.shp.close() 1105 | self.saveShx(target) 1106 | self.shx.close() 1107 | self.saveDbf(target) 1108 | self.dbf.close() 1109 | if generated: 1110 | return target 1111 | class Editor(Writer): 1112 | def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1): 1113 | self.autoBalance = autoBalance 1114 | if not shapefile: 1115 | Writer.__init__(self, shapeType) 1116 | elif is_string(shapefile): 1117 | base = os.path.splitext(shapefile)[0] 1118 | if os.path.isfile("%s.shp" % base): 1119 | r = Reader(base) 1120 | Writer.__init__(self, r.shapeType) 1121 | self._shapes = r.shapes() 1122 | self.fields = r.fields 1123 | self.records = r.records() 1124 | 1125 | def select(self, expr): 1126 | """Select one or more shapes (to be implemented)""" 1127 | # TODO: Implement expressions to select shapes. 1128 | pass 1129 | 1130 | def delete(self, shape=None, part=None, point=None): 1131 | """Deletes the specified part of any shape by specifying a shape 1132 | number, part number, or point number.""" 1133 | # shape, part, point 1134 | if shape and part and point: 1135 | del self._shapes[shape][part][point] 1136 | # shape, part 1137 | elif shape and part and not point: 1138 | del self._shapes[shape][part] 1139 | # shape 1140 | elif shape and not part and not point: 1141 | del self._shapes[shape] 1142 | # point 1143 | elif not shape and not part and point: 1144 | for s in self._shapes: 1145 | if s.shapeType == 1: 1146 | del self._shapes[point] 1147 | else: 1148 | for part in s.parts: 1149 | del s[part][point] 1150 | # part, point 1151 | elif not shape and part and point: 1152 | for s in self._shapes: 1153 | del s[part][point] 1154 | # part 1155 | elif not shape and part and not point: 1156 | for s in self._shapes: 1157 | del s[part] 1158 | 1159 | def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None): 1160 | """Creates/updates a point shape. The arguments allows 1161 | you to update a specific point by shape, part, point of any 1162 | shape type.""" 1163 | # shape, part, point 1164 | if shape and part and point: 1165 | try: self._shapes[shape] 1166 | except IndexError: self._shapes.append([]) 1167 | try: self._shapes[shape][part] 1168 | except IndexError: self._shapes[shape].append([]) 1169 | try: self._shapes[shape][part][point] 1170 | except IndexError: self._shapes[shape][part].append([]) 1171 | p = self._shapes[shape][part][point] 1172 | if x: p[0] = x 1173 | if y: p[1] = y 1174 | if z: p[2] = z 1175 | if m: p[3] = m 1176 | self._shapes[shape][part][point] = p 1177 | # shape, part 1178 | elif shape and part and not point: 1179 | try: self._shapes[shape] 1180 | except IndexError: self._shapes.append([]) 1181 | try: self._shapes[shape][part] 1182 | except IndexError: self._shapes[shape].append([]) 1183 | points = self._shapes[shape][part] 1184 | for i in xrange(len(points)): 1185 | p = points[i] 1186 | if x: p[0] = x 1187 | if y: p[1] = y 1188 | if z: p[2] = z 1189 | if m: p[3] = m 1190 | self._shapes[shape][part][i] = p 1191 | # shape 1192 | elif shape and not part and not point: 1193 | try: self._shapes[shape] 1194 | except IndexError: self._shapes.append([]) 1195 | 1196 | # point 1197 | # part 1198 | if addr: 1199 | shape, part, point = addr 1200 | self._shapes[shape][part][point] = [x, y, z, m] 1201 | else: 1202 | Writer.point(self, x, y, z, m) 1203 | if self.autoBalance: 1204 | self.balance() 1205 | 1206 | def validate(self): 1207 | """An optional method to try and validate the shapefile 1208 | as much as possible before writing it (not implemented).""" 1209 | #TODO: Implement validation method 1210 | pass 1211 | 1212 | def balance(self): 1213 | """Adds a corresponding empty attribute or null geometry record depending 1214 | on which type of record was created to make sure all three files 1215 | are in synch.""" 1216 | if len(self.records) > len(self._shapes): 1217 | self.null() 1218 | elif len(self.records) < len(self._shapes): 1219 | self.record() 1220 | 1221 | def __fieldNorm(self, fieldName): 1222 | """Normalizes a dbf field name to fit within the spec and the 1223 | expectations of certain ESRI software.""" 1224 | if len(fieldName) > 11: fieldName = fieldName[:11] 1225 | fieldName = fieldName.upper() 1226 | fieldName.replace(' ', '_') 1227 | 1228 | # Begin Testing 1229 | def test(): 1230 | import doctest 1231 | doctest.NORMALIZE_WHITESPACE = 1 1232 | doctest.testfile("README.txt", verbose=1) 1233 | 1234 | if __name__ == "__main__": 1235 | """ 1236 | Doctests are contained in the file 'README.txt'. This library was originally developed 1237 | using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in 1238 | testing libraries but for now unit testing is done using what's available in 1239 | 2.3. 1240 | """ 1241 | test() 1242 | -------------------------------------------------------------------------------- /geovis/textual.py: -------------------------------------------------------------------------------- 1 | # Import main modules 2 | import decimal 3 | 4 | def txt(obj, encoding="utf-8"): 5 | if isinstance(obj, basestring): 6 | if not isinstance(obj, unicode): 7 | try: 8 | obj = unicode(obj, encoding) 9 | except: 10 | obj = unicode(obj, "latin") #backup encoding to decode into 11 | else: 12 | try: 13 | obj = str(float(obj)) 14 | except: 15 | obj = str(obj) 16 | return obj 17 | 18 | def encode(obj, encoding="utf-8", strlen=150, floatlen=16, floatprec=6): 19 | try: 20 | #encode as decimal nr 21 | float(obj) 22 | decimal.getcontext().prec = floatprec 23 | obj = str(decimal.Decimal(str(obj))) [:floatlen] 24 | except: 25 | #encode as text 26 | try: 27 | obj = obj.encode(encoding) 28 | except: 29 | obj = obj.encode("latin") #backup encoding to encode as 30 | return obj 31 | 32 | -------------------------------------------------------------------------------- /geovis/timetaker.py: -------------------------------------------------------------------------------- 1 | # timer module to easily measure time in any code 2 | # works like a stopclock and reports back/prints the time taken 3 | # when you call stop 4 | 5 | import time 6 | runningtimers = dict() 7 | 8 | def start(name="unnamed"): 9 | starttime = time.clock() 10 | runningtimers.update([(name,starttime)]) 11 | 12 | def stop(name="unnamed"): 13 | endtime = time.clock() 14 | #obtain starttime based on name provided 15 | #but if no name was provided at starttime it will still get the time from the default 16 | starttime = runningtimers.get(name, None) 17 | if not starttime: 18 | starttime = runningtimers["unnamed"] 19 | timetaken = endtime-starttime 20 | if timetaken < 60.0: 21 | timeunit = "seconds" 22 | if timetaken > 60.0: 23 | timetaken = timetaken/60.0 24 | timeunit = "minutes" 25 | if timetaken > 60.0: 26 | timetaken = timetaken/60.0 27 | timeunit = "hours" 28 | #finally, print time taken 29 | print name, timetaken, timeunit 30 | if name in runningtimers: 31 | runningtimers.pop(name) 32 | -------------------------------------------------------------------------------- /images/PIL_normal(old)_vs_antialiased(new).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karimbahgat/GeoVis/835e23519f5e746ce962818e1ee5262436a39df4/images/PIL_normal(old)_vs_antialiased(new).png -------------------------------------------------------------------------------- /images/readme_topbanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karimbahgat/GeoVis/835e23519f5e746ce962818e1ee5262436a39df4/images/readme_topbanner.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Karim Bahgat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. --------------------------------------------------------------------------------