├── README.md ├── dotfile.py ├── dotmap.pde ├── globalmaptiles.pde └── globalmaptiles.py /README.md: -------------------------------------------------------------------------------- 1 | RacialDotMap 2 | ============ 3 | 4 | Python and Processing code for creating (1) a dataset for every person in the U.S., coded by race and ethnicity, and (2) map tiles from the data. 5 | 6 | http://demographics.coopercenter.org/DotMap/index.html 7 | 8 | The code was adapted from a similar project by Brandon Martin-Anderson from the MIT Media Lab. 9 | -------------------------------------------------------------------------------- /dotfile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from osgeo import ogr 3 | from shapely.wkb import loads 4 | from shapely.geometry import * 5 | from random import uniform 6 | import sqlite3 7 | 8 | # Import the module that converts spatial data between formats 9 | 10 | sys.path.append("filepath") 11 | from globalmaptiles import GlobalMercator 12 | 13 | # Main function that reads the shapefile, obtains the population counts, 14 | # creates a point object for each person by race, and exports to a SQL database. 15 | 16 | def main(input_filename, output_filename): 17 | 18 | # Create a GlobalMercator object for later conversions 19 | 20 | merc = GlobalMercator() 21 | 22 | # Open the shapefile 23 | 24 | ds = ogr.Open(input_filename) 25 | 26 | if ds is None: 27 | print "Open failed.\n" 28 | sys.exit( 1 ) 29 | 30 | # Obtain the first (and only) layer in the shapefile 31 | 32 | lyr = ds.GetLayerByIndex(0) 33 | 34 | lyr.ResetReading() 35 | 36 | # Obtain the field definitions in the shapefile layer 37 | 38 | feat_defn = lyr.GetLayerDefn() 39 | field_defns = [feat_defn.GetFieldDefn(i) for i in range(feat_defn.GetFieldCount())] 40 | 41 | # Obtain the index of the field for the count for whites, blacks, Asians, 42 | # Others, and Hispanics. 43 | 44 | for i, defn in enumerate(field_defns): 45 | 46 | if defn.GetName() == "POP10": 47 | pop_field = i 48 | 49 | if defn.GetName() == "nh_white_n": 50 | white_field = i 51 | 52 | if defn.GetName() == "nh_black_n": 53 | black_field = i 54 | 55 | if defn.GetName() == "nh_asian_n": 56 | asian_field = i 57 | 58 | if defn.GetName() == "hispanic_n": 59 | hispanic_field = i 60 | 61 | if defn.GetName() == "NH_Other_n": 62 | other_field = i 63 | 64 | if defn.GetName() == "STATEFP10": 65 | statefips_field = i 66 | 67 | # Set-up the output file 68 | 69 | conn = sqlite3.connect( output_filename ) 70 | c = conn.cursor() 71 | c.execute( "create table if not exists people_by_race (statefips text, x text, y text, quadkey text, race_type text)" ) 72 | 73 | # Obtain the number of features (Census Blocks) in the layer 74 | 75 | n_features = len(lyr) 76 | 77 | # Iterate through every feature (Census Block Ploygon) in the layer, 78 | # obtain the population counts, and create a point for each person within 79 | # that feature. 80 | 81 | for j, feat in enumerate( lyr ): 82 | 83 | # Print a progress read-out for every 1000 features and export to hard disk 84 | 85 | if j % 1000 == 0: 86 | conn.commit() 87 | print "%s/%s (%0.2f%%)"%(j+1,n_features,100*((j+1)/float(n_features))) 88 | 89 | # Obtain total population, racial counts, and state fips code of the individual census block 90 | 91 | pop = int(feat.GetField(pop_field)) 92 | white = int(feat.GetField(white_field)) 93 | black = int(feat.GetField(black_field)) 94 | asian = int(feat.GetField(asian_field)) 95 | hispanic = int(feat.GetField(hispanic_field)) 96 | other = int(feat.GetField(other_field)) 97 | statefips = feat.GetField(statefips_field) 98 | 99 | # Obtain the OGR polygon object from the feature 100 | 101 | geom = feat.GetGeometryRef() 102 | 103 | if geom is None: 104 | continue 105 | 106 | # Convert the OGR Polygon into a Shapely Polygon 107 | 108 | poly = loads(geom.ExportToWkb()) 109 | 110 | if poly is None: 111 | continue 112 | 113 | # Obtain the "boundary box" of extreme points of the polygon 114 | 115 | bbox = poly.bounds 116 | 117 | if not bbox: 118 | continue 119 | 120 | leftmost,bottommost,rightmost,topmost = bbox 121 | 122 | # Generate a point object within the census block for every person by race 123 | 124 | for i in range(white): 125 | 126 | # Choose a random longitude and latitude within the boundary box 127 | # and within the orginial ploygon of the census block 128 | 129 | while True: 130 | 131 | samplepoint = Point(uniform(leftmost, rightmost),uniform(bottommost, topmost)) 132 | 133 | if samplepoint is None: 134 | break 135 | 136 | if poly.contains(samplepoint): 137 | break 138 | 139 | # Convert the longitude and latitude coordinates to meters and 140 | # a tile reference 141 | 142 | x, y = merc.LatLonToMeters(samplepoint.y,samplepoint.x) 143 | tx,ty = merc.MetersToTile(x, y, 21) 144 | 145 | # Create a unique quadkey for each point object 146 | 147 | quadkey = merc.QuadTree(tx, ty, 21) 148 | 149 | # Create categorical variable for the race category 150 | 151 | race_type = 'w' 152 | 153 | # Export data to the database file 154 | 155 | c.execute( "insert into people_by_race values (?,?,?,?,?)", (statefips, x, y, quadkey,race_type) ) 156 | 157 | for i in range(black): 158 | 159 | # Choose a random longitude and latitude within the boundary box 160 | # points and within the orginial ploygon of the census block 161 | 162 | while True: 163 | 164 | samplepoint = Point(uniform(leftmost, rightmost),uniform(bottommost, topmost)) 165 | 166 | if samplepoint is None: 167 | break 168 | 169 | if poly.contains(samplepoint): 170 | break 171 | 172 | # Convert the longitude and latitude coordinates to meters and 173 | # a tile reference 174 | 175 | x, y = merc.LatLonToMeters(samplepoint.y,samplepoint.x) 176 | tx,ty = merc.MetersToTile(x, y, 21) 177 | 178 | # Create a unique quadkey for each point object 179 | 180 | quadkey = merc.QuadTree(tx, ty, 21) 181 | 182 | # Create categorical variable for the race category 183 | 184 | race_type = 'b' 185 | 186 | # Export data to the database file 187 | 188 | c.execute( "insert into people_by_race values (?,?,?,?,?)", (statefips, x, y, quadkey,race_type) ) 189 | 190 | for i in range(asian): 191 | 192 | # Choose a random longitude and latitude within the boundary box 193 | # points and within the orginial ploygon of the census block 194 | 195 | while True: 196 | 197 | samplepoint = Point(uniform(leftmost, rightmost),uniform(bottommost, topmost)) 198 | 199 | if samplepoint is None: 200 | break 201 | 202 | if poly.contains(samplepoint): 203 | break 204 | 205 | # Convert the longitude and latitude coordinates to meters and 206 | # a tile reference 207 | 208 | x, y = merc.LatLonToMeters(samplepoint.y,samplepoint.x) 209 | tx,ty = merc.MetersToTile(x, y, 21) 210 | 211 | # Create a unique quadkey for each point object 212 | 213 | quadkey = merc.QuadTree(tx, ty, 21) 214 | 215 | # Create categorical variable for the race category 216 | 217 | race_type = 'a' 218 | 219 | # Export data to the database file 220 | 221 | c.execute( "insert into people_by_race values (?,?,?,?,?)", (statefips, x, y, quadkey,race_type) ) 222 | 223 | for i in range(hispanic): 224 | 225 | # Choose a random longitude and latitude within the boundary box 226 | # points and within the orginial ploygon of the census block 227 | 228 | while True: 229 | 230 | samplepoint = Point(uniform(leftmost, rightmost),uniform(bottommost, topmost)) 231 | 232 | if samplepoint is None: 233 | break 234 | 235 | if poly.contains(samplepoint): 236 | break 237 | 238 | # Convert the longitude and latitude coordinates to meters and 239 | # a tile reference 240 | 241 | x, y = merc.LatLonToMeters(samplepoint.y,samplepoint.x) 242 | tx,ty = merc.MetersToTile(x, y, 21) 243 | 244 | # Create a unique quadkey for each point object 245 | 246 | quadkey = merc.QuadTree(tx, ty, 21) 247 | 248 | # Create categorical variable for the race category 249 | 250 | race_type = 'h' 251 | 252 | # Export data to the database file 253 | 254 | c.execute( "insert into people_by_race values (?,?,?,?,?)", (statefips, x, y, quadkey,race_type) ) 255 | 256 | for i in range(other): 257 | 258 | # Choose a random longitude and latitude within the boundary box 259 | # points and within the orginial ploygon of the census block 260 | 261 | while True: 262 | 263 | samplepoint = Point(uniform(leftmost, rightmost),uniform(bottommost, topmost)) 264 | 265 | if samplepoint is None: 266 | break 267 | 268 | if poly.contains(samplepoint): 269 | break 270 | 271 | # Convert the longitude and latitude coordinates to meters and 272 | # a tile reference 273 | 274 | x, y = merc.LatLonToMeters(samplepoint.y,samplepoint.x) 275 | tx,ty = merc.MetersToTile(x, y, 21) 276 | 277 | # Create a unique quadkey for each point object 278 | 279 | quadkey = merc.QuadTree(tx, ty, 21) 280 | 281 | # Create categorical variable for the race category 282 | 283 | race_type = 'o' 284 | 285 | # Export data to the database file 286 | 287 | c.execute( "insert into people_by_race values (?,?,?,?,?)", (statefips, x, y, quadkey,race_type) ) 288 | 289 | conn.commit() 290 | 291 | # Execution code... 292 | 293 | if __name__=='__main__': 294 | 295 | for state in ['01','02','04','05','06','08','09','10','11','12','13','15', 296 | '16','17','18','19','20','21','22','23','24','25','26','27','28','29','30', 297 | '31','32','33','34','35','36','37','38','39','40','41','42','44','45','46', 298 | '47','48','49','50','51','53','54','55','56']: 299 | 300 | print "state:%s"%state 301 | 302 | main( ".../Census Data/statefile_"+state+".shp", 303 | ".../Map Data/people_by_race5.db") 304 | -------------------------------------------------------------------------------- /dotmap.pde: -------------------------------------------------------------------------------- 1 | BufferedReader reader; 2 | double ll, bb, rr, tt; 3 | 4 | float A = 1000.0; 5 | 6 | GlobalMercator proj = new GlobalMercator(); 7 | 8 | class PersonPoint { 9 | double x, y; 10 | String quadnode; 11 | 12 | PersonPoint(String row) { 13 | String[] fields = split(row, ","); 14 | this.x = Double.parseDouble(fields[1])/A; 15 | this.y = Double.parseDouble(fields[2])/A; 16 | this.quadnode = fields[3]; 17 | } 18 | 19 | void draw(PGraphics pg) { 20 | pg.point((float)this.x, (float)this.y); 21 | } 22 | } 23 | 24 | ArrayList people; 25 | 26 | float pointWeight(int level) { 27 | switch(level) { 28 | case 4: 29 | return 0.01; 30 | case 5: 31 | return 0.01; 32 | case 6: 33 | return 0.01; 34 | case 7: 35 | return 0.01; 36 | case 8: 37 | return 0.01; 38 | case 9: 39 | return 0.02; 40 | case 10: 41 | return 0.02; 42 | case 11: 43 | return 0.02; 44 | case 12: 45 | return 0.02; 46 | case 13: 47 | return 0.02; 48 | default: 49 | return 0.0; 50 | } 51 | } 52 | 53 | float transparent(int level) { 54 | switch(level) { 55 | case 4: 56 | return 153; 57 | case 5: 58 | return 153; 59 | case 6: 60 | return 179; 61 | case 7: 62 | return 179; 63 | case 8: 64 | return 204; 65 | case 9: 66 | return 204; 67 | case 10: 68 | return 230; 69 | case 11: 70 | return 230; 71 | case 12: 72 | return 255; 73 | case 13: 74 | return 255; 75 | default: 76 | return 0.0; 77 | } 78 | } 79 | 80 | void setup(){ 81 | 82 | size(512, 512, JAVA2D); 83 | smooth(8); 84 | 85 | String[] zoomlevels = loadStrings(".../zoomlevel.txt"); // a text file with lines 4, 5, 6 ...etc. specifing zoomlevel 86 | 87 | for ( int i=0; i Meters <-> Pixels <-> Tile 18 | // 19 | // WGS84 coordinates Spherical Mercator Pixels in pyramid Tiles in pyramid 20 | // lat/lon XY in metres XY pixels Z zoom XYZ from TMS 21 | // EPSG:4326 EPSG:900913 22 | // .----. --------- -- TMS 23 | // / \ <-> | | <-> /----/ <-> Google 24 | // \ / | | /--------/ QuadTree 25 | // ----- --------- /------------/ 26 | // KML, public WebMapService Web Clients TileMapService 27 | // 28 | // What is the coordinate extent of Earth in EPSG:900913? 29 | // 30 | // [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] 31 | // Constant 20037508.342789244 comes from the circumference of the Earth in meters, 32 | // which is 40 thousand kilometers, the coordinate origin is in the middle of extent. 33 | // In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0 34 | // $ echo 180 85 | gdaltransform -s_srs EPSG:4326 -t_srs EPSG:900913 35 | // Polar areas with abs(latitude) bigger then 85.05112878 are clipped off. 36 | // 37 | // What are zoom level constants (pixels/meter) for pyramid with EPSG:900913? 38 | // 39 | // whole region is on top of pyramid (zoom=0) covered by 256x256 pixels tile, 40 | // every lower zoom level resolution is always divided by two 41 | // initialResolution = 20037508.342789244 * 2 / 256 = 156543.03392804062 42 | // 43 | // What is the difference between TMS and Google Maps/QuadTree tile name convention? 44 | // 45 | // The tile raster itself is the same (equal extent, projection, pixel size), 46 | // there is just different identification of the same raster tile. 47 | // Tiles in TMS are counted from [0,0] in the bottom-left corner, id is XYZ. 48 | // Google placed the origin [0,0] to the top-left corner, reference is XYZ. 49 | // Microsoft is referencing tiles by a QuadTree name, defined on the website: 50 | // http://msdn2.microsoft.com/en-us/library/bb259689.aspx 51 | // 52 | // The lat/lon coordinates are using WGS84 datum, yeh? 53 | // 54 | // Yes, all lat/lon we are mentioning should use WGS84 Geodetic Datum. 55 | // Well, the web clients like Google Maps are projecting those coordinates by 56 | // Spherical Mercator, so in fact lat/lon coordinates on sphere are treated as if 57 | // the were on the WGS84 ellipsoid. 58 | // 59 | // From MSDN documentation: 60 | // To simplify the calculations, we use the spherical form of projection, not 61 | // the ellipsoidal form. Since the projection is used only for map display, 62 | // and not for displaying numeric coordinates, we don't need the extra precision 63 | // of an ellipsoidal projection. The spherical projection causes approximately 64 | // 0.33 percent scale distortion in the Y direction, which is not visually noticable. 65 | // 66 | // How do I create a raster in EPSG:900913 and convert coordinates with PROJ.4? 67 | // 68 | // You can use standard GIS tools like gdalwarp, cs2cs or gdaltransform. 69 | // All of the tools supports -t_srs 'epsg:900913'. 70 | // 71 | // For other GIS programs check the exact definition of the projection: 72 | // More info at http://spatialreference.org/ref/user/google-projection/ 73 | // The same projection is degined as EPSG:3785. WKT definition is in the official 74 | // EPSG database. 75 | // 76 | // Proj4 Text: 77 | // +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 78 | // +k=1.0 +units=m +nadgrids=@null +no_defs 79 | // 80 | // Human readable WKT format of EPGS:900913: 81 | // PROJCS["Google Maps Global Mercator", 82 | // GEOGCS["WGS 84", 83 | // DATUM["WGS_1984", 84 | // SPHEROID["WGS 84",6378137,298.2572235630016, 85 | // AUTHORITY["EPSG","7030"]], 86 | // AUTHORITY["EPSG","6326"]], 87 | // PRIMEM["Greenwich",0], 88 | // UNIT["degree",0.0174532925199433], 89 | // AUTHORITY["EPSG","4326"]], 90 | // PROJECTION["Mercator_1SP"], 91 | // PARAMETER["central_meridian",0], 92 | // PARAMETER["scale_factor",1], 93 | // PARAMETER["false_easting",0], 94 | // PARAMETER["false_northing",0], 95 | // UNIT["metre",1, 96 | // AUTHORITY["EPSG","9001"]]] 97 | 98 | int tileSize=256; 99 | float initialResolution; 100 | float originShift; 101 | 102 | GlobalMercator() { 103 | //Initialize the TMS Global Mercator pyramid" 104 | this.initialResolution = 2 * PI * 6378137 / this.tileSize; 105 | //156543.03392804062 for tileSize 256 pixels 106 | this.originShift = 2 * PI * 6378137 / 2.0; 107 | //20037508.342789244 108 | } 109 | 110 | PVector LatLonToMeters(float lat, float lon ) { 111 | //"Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913" 112 | 113 | float mx = lon * this.originShift / 180.0; 114 | float my = log( tan((90 + lat) * PI / 360.0 )) / (PI / 180.0); 115 | 116 | my = my * this.originShift / 180.0; 117 | return new PVector(mx, my); 118 | } 119 | 120 | PVector MetersToLatLon(float mx, float my ) { 121 | //"Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum" 122 | 123 | float lon = (mx / this.originShift) * 180.0; 124 | float lat = (my / this.originShift) * 180.0; 125 | 126 | lat = 180 / PI * (2 * atan( exp( lat * PI / 180.0)) - PI / 2.0); 127 | return new PVector(lat, lon); 128 | } 129 | 130 | PVector PixelsToMeters(float px, float py, int zoom) { 131 | //"Converts pixel coordinates in given zoom level of pyramid to EPSG:900913" 132 | 133 | float res = this.Resolution( zoom ); 134 | float mx = px * res - this.originShift; 135 | float my = py * res - this.originShift; 136 | return new PVector(mx, my); 137 | } 138 | 139 | PVector MetersToPixels(float mx, float my, int zoom) { 140 | //"Converts EPSG:900913 to pyramid pixel coordinates in given zoom level" 141 | 142 | float res = this.Resolution( zoom ); 143 | float px = (mx + this.originShift) / res; 144 | float py = (my + this.originShift) / res; 145 | return new PVector( px, py ); 146 | } 147 | 148 | PVector PixelsToTile( float px, float py) { 149 | //"Returns a tile covering region in given pixel coordinates" 150 | 151 | int tx = int( ceil( px / float(this.tileSize) ) - 1 ); 152 | int ty = int( ceil( py / float(this.tileSize) ) - 1 ); 153 | return new PVector(tx, ty); 154 | } 155 | 156 | PVector PixelsToRaster(float px, float py, int zoom) { 157 | //"Move the origin of pixel coordinates to top-left corner" 158 | 159 | int mapSize = this.tileSize << zoom; 160 | return new PVector( px, mapSize - py ); 161 | } 162 | 163 | PVector MetersToTile(float mx, float my, int zoom) { 164 | //"Returns tile for given mercator coordinates" 165 | 166 | PVector pp = this.MetersToPixels( mx, my, zoom); 167 | return this.PixelsToTile( pp.x, pp.y ); 168 | } 169 | 170 | PVector[] TileBounds(int tx, int ty, int zoom) { 171 | //"Returns bounds of the given tile in EPSG:900913 coordinates" 172 | 173 | PVector ll = this.PixelsToMeters( tx*this.tileSize, ty*this.tileSize, zoom ); 174 | PVector ur = this.PixelsToMeters( (tx+1)*this.tileSize, (ty+1)*this.tileSize, zoom ); 175 | 176 | PVector[] ret = new PVector[2]; 177 | ret[0]=ll; 178 | ret[1]=ur; 179 | return ret; 180 | } 181 | 182 | // def TileLatLonBounds(self, tx, ty, zoom ): 183 | // "Returns bounds of the given tile in latutude/longitude using WGS84 datum" 184 | // 185 | // bounds = self.TileBounds( tx, ty, zoom) 186 | // minLat, minLon = self.MetersToLatLon(bounds[0], bounds[1]) 187 | // maxLat, maxLon = self.MetersToLatLon(bounds[2], bounds[3]) 188 | // 189 | // return ( minLat, minLon, maxLat, maxLon ) 190 | 191 | float Resolution( int zoom ) { 192 | //"Resolution (meters/pixel) for given zoom level (measured at Equator)" 193 | 194 | //# return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom) 195 | return this.initialResolution / (pow(2, zoom)); 196 | } 197 | 198 | // def ZoomForPixelSize(self, pixelSize ): 199 | // "Maximal scaledown zoom of the pyramid closest to the pixelSize." 200 | // 201 | // for i in range(30): 202 | // if pixelSize > self.Resolution(i): 203 | // return i-1 if i!=0 else 0 # We don't want to scale up 204 | // 205 | PVector GoogleTile(int tx, int ty, int zoom) { 206 | //"Converts TMS tile coordinates to Google Tile coordinates" 207 | 208 | // coordinate origin is moved from bottom-left to top-left corner of the extent 209 | return new PVector(tx, (pow(2, zoom) - 1) - ty); 210 | } 211 | 212 | // 213 | // def QuadTree(self, tx, ty, zoom ): 214 | // "Converts TMS tile coordinates to Microsoft QuadTree" 215 | // 216 | // quadKey = "" 217 | // ty = (2**zoom - 1) - ty 218 | // for i in range(zoom, 0, -1): 219 | // digit = 0 220 | // mask = 1 << (i-1) 221 | // if (tx & mask) != 0: 222 | // digit += 1 223 | // if (ty & mask) != 0: 224 | // digit += 2 225 | // quadKey += str(digit) 226 | // 227 | // return quadKey 228 | 229 | PVector QuadKeyToTileXY(String quadKey) 230 | { 231 | int tileX; 232 | int tileY; 233 | int levelOfDetail; 234 | 235 | tileX = tileY = 0; 236 | levelOfDetail = quadKey.length(); 237 | for (int i = levelOfDetail; i > 0; i--) 238 | { 239 | int mask = 1 << (i - 1); 240 | switch (quadKey.charAt(levelOfDetail - i)) 241 | { 242 | case '0': 243 | break; 244 | 245 | case '1': 246 | tileX |= mask; 247 | break; 248 | 249 | case '2': 250 | tileY |= mask; 251 | break; 252 | 253 | case '3': 254 | tileX |= mask; 255 | tileY |= mask; 256 | break; 257 | 258 | } 259 | } 260 | 261 | return new PVector(tileX,tileY,levelOfDetail); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /globalmaptiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ############################################################################### 3 | # $Id$ 4 | # 5 | # Project: GDAL2Tiles, Google Summer of Code 2007 & 2008 6 | # Global Map Tiles Classes 7 | # Purpose: Convert a raster into TMS tiles, create KML SuperOverlay EPSG:4326, 8 | # generate a simple HTML viewers based on Google Maps and OpenLayers 9 | # Author: Klokan Petr Pridal, klokan at klokan dot cz 10 | # Web: http://www.klokan.cz/projects/gdal2tiles/ 11 | # 12 | ############################################################################### 13 | # Copyright (c) 2008 Klokan Petr Pridal. All rights reserved. 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a 16 | # copy of this software and associated documentation files (the "Software"), 17 | # to deal in the Software without restriction, including without limitation 18 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 19 | # and/or sell copies of the Software, and to permit persons to whom the 20 | # Software is furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included 23 | # in all copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 26 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 28 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 30 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 31 | # DEALINGS IN THE SOFTWARE. 32 | ############################################################################### 33 | 34 | """ 35 | globalmaptiles.py 36 | 37 | Global Map Tiles as defined in Tile Map Service (TMS) Profiles 38 | ============================================================== 39 | 40 | Functions necessary for generation of global tiles used on the web. 41 | It contains classes implementing coordinate conversions for: 42 | 43 | - GlobalMercator (based on EPSG:900913 = EPSG:3785) 44 | for Google Maps, Yahoo Maps, Microsoft Maps compatible tiles 45 | - GlobalGeodetic (based on EPSG:4326) 46 | for OpenLayers Base Map and Google Earth compatible tiles 47 | 48 | More info at: 49 | 50 | http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification 51 | http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation 52 | http://msdn.microsoft.com/en-us/library/bb259689.aspx 53 | http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates 54 | 55 | Created by Klokan Petr Pridal on 2008-07-03. 56 | Google Summer of Code 2008, project GDAL2Tiles for OSGEO. 57 | 58 | In case you use this class in your product, translate it to another language 59 | or find it usefull for your project please let me know. 60 | My email: klokan at klokan dot cz. 61 | I would like to know where it was used. 62 | 63 | Class is available under the open-source GDAL license (www.gdal.org). 64 | """ 65 | 66 | import math 67 | 68 | class GlobalMercator(object): 69 | """ 70 | TMS Global Mercator Profile 71 | --------------------------- 72 | 73 | Functions necessary for generation of tiles in Spherical Mercator projection, 74 | EPSG:900913 (EPSG:gOOglE, Google Maps Global Mercator), EPSG:3785, OSGEO:41001. 75 | 76 | Such tiles are compatible with Google Maps, Microsoft Virtual Earth, Yahoo Maps, 77 | UK Ordnance Survey OpenSpace API, ... 78 | and you can overlay them on top of base maps of those web mapping applications. 79 | 80 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). 81 | 82 | What coordinate conversions do we need for TMS Global Mercator tiles:: 83 | 84 | LatLon <-> Meters <-> Pixels <-> Tile 85 | 86 | WGS84 coordinates Spherical Mercator Pixels in pyramid Tiles in pyramid 87 | lat/lon XY in metres XY pixels Z zoom XYZ from TMS 88 | EPSG:4326 EPSG:900913 89 | .----. --------- -- TMS 90 | / \ <-> | | <-> /----/ <-> Google 91 | \ / | | /--------/ QuadTree 92 | ----- --------- /------------/ 93 | KML, public WebMapService Web Clients TileMapService 94 | 95 | What is the coordinate extent of Earth in EPSG:900913? 96 | 97 | [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] 98 | Constant 20037508.342789244 comes from the circumference of the Earth in meters, 99 | which is 40 thousand kilometers, the coordinate origin is in the middle of extent. 100 | In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0 101 | $ echo 180 85 | gdaltransform -s_srs EPSG:4326 -t_srs EPSG:900913 102 | Polar areas with abs(latitude) bigger then 85.05112878 are clipped off. 103 | 104 | What are zoom level constants (pixels/meter) for pyramid with EPSG:900913? 105 | 106 | whole region is on top of pyramid (zoom=0) covered by 256x256 pixels tile, 107 | every lower zoom level resolution is always divided by two 108 | initialResolution = 20037508.342789244 * 2 / 256 = 156543.03392804062 109 | 110 | What is the difference between TMS and Google Maps/QuadTree tile name convention? 111 | 112 | The tile raster itself is the same (equal extent, projection, pixel size), 113 | there is just different identification of the same raster tile. 114 | Tiles in TMS are counted from [0,0] in the bottom-left corner, id is XYZ. 115 | Google placed the origin [0,0] to the top-left corner, reference is XYZ. 116 | Microsoft is referencing tiles by a QuadTree name, defined on the website: 117 | http://msdn2.microsoft.com/en-us/library/bb259689.aspx 118 | 119 | The lat/lon coordinates are using WGS84 datum, yeh? 120 | 121 | Yes, all lat/lon we are mentioning should use WGS84 Geodetic Datum. 122 | Well, the web clients like Google Maps are projecting those coordinates by 123 | Spherical Mercator, so in fact lat/lon coordinates on sphere are treated as if 124 | the were on the WGS84 ellipsoid. 125 | 126 | From MSDN documentation: 127 | To simplify the calculations, we use the spherical form of projection, not 128 | the ellipsoidal form. Since the projection is used only for map display, 129 | and not for displaying numeric coordinates, we don't need the extra precision 130 | of an ellipsoidal projection. The spherical projection causes approximately 131 | 0.33 percent scale distortion in the Y direction, which is not visually noticable. 132 | 133 | How do I create a raster in EPSG:900913 and convert coordinates with PROJ.4? 134 | 135 | You can use standard GIS tools like gdalwarp, cs2cs or gdaltransform. 136 | All of the tools supports -t_srs 'epsg:900913'. 137 | 138 | For other GIS programs check the exact definition of the projection: 139 | More info at http://spatialreference.org/ref/user/google-projection/ 140 | The same projection is degined as EPSG:3785. WKT definition is in the official 141 | EPSG database. 142 | 143 | Proj4 Text: 144 | +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 145 | +k=1.0 +units=m +nadgrids=@null +no_defs 146 | 147 | Human readable WKT format of EPGS:900913: 148 | PROJCS["Google Maps Global Mercator", 149 | GEOGCS["WGS 84", 150 | DATUM["WGS_1984", 151 | SPHEROID["WGS 84",6378137,298.2572235630016, 152 | AUTHORITY["EPSG","7030"]], 153 | AUTHORITY["EPSG","6326"]], 154 | PRIMEM["Greenwich",0], 155 | UNIT["degree",0.0174532925199433], 156 | AUTHORITY["EPSG","4326"]], 157 | PROJECTION["Mercator_1SP"], 158 | PARAMETER["central_meridian",0], 159 | PARAMETER["scale_factor",1], 160 | PARAMETER["false_easting",0], 161 | PARAMETER["false_northing",0], 162 | UNIT["metre",1, 163 | AUTHORITY["EPSG","9001"]]] 164 | """ 165 | 166 | def __init__(self, tileSize=256): 167 | "Initialize the TMS Global Mercator pyramid" 168 | self.tileSize = tileSize 169 | self.initialResolution = 2 * math.pi * 6378137 / self.tileSize 170 | # 156543.03392804062 for tileSize 256 pixels 171 | self.originShift = 2 * math.pi * 6378137 / 2.0 172 | # 20037508.342789244 173 | 174 | def LatLonToMeters(self, lat, lon ): 175 | "Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913" 176 | 177 | mx = lon * self.originShift / 180.0 178 | my = math.log( math.tan((90 + lat) * math.pi / 360.0 )) / (math.pi / 180.0) 179 | 180 | my = my * self.originShift / 180.0 181 | return mx, my 182 | 183 | def MetersToLatLon(self, mx, my ): 184 | "Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum" 185 | 186 | lon = (mx / self.originShift) * 180.0 187 | lat = (my / self.originShift) * 180.0 188 | 189 | lat = 180 / math.pi * (2 * math.atan( math.exp( lat * math.pi / 180.0)) - math.pi / 2.0) 190 | return lat, lon 191 | 192 | def PixelsToMeters(self, px, py, zoom): 193 | "Converts pixel coordinates in given zoom level of pyramid to EPSG:900913" 194 | 195 | res = self.Resolution( zoom ) 196 | mx = px * res - self.originShift 197 | my = py * res - self.originShift 198 | return mx, my 199 | 200 | def MetersToPixels(self, mx, my, zoom): 201 | "Converts EPSG:900913 to pyramid pixel coordinates in given zoom level" 202 | 203 | res = self.Resolution( zoom ) 204 | px = (mx + self.originShift) / res 205 | py = (my + self.originShift) / res 206 | return px, py 207 | 208 | def PixelsToTile(self, px, py): 209 | "Returns a tile covering region in given pixel coordinates" 210 | 211 | tx = int( math.ceil( px / float(self.tileSize) ) - 1 ) 212 | ty = int( math.ceil( py / float(self.tileSize) ) - 1 ) 213 | return tx, ty 214 | 215 | def PixelsToRaster(self, px, py, zoom): 216 | "Move the origin of pixel coordinates to top-left corner" 217 | 218 | mapSize = self.tileSize << zoom 219 | return px, mapSize - py 220 | 221 | def MetersToTile(self, mx, my, zoom): 222 | "Returns tile for given mercator coordinates" 223 | 224 | px, py = self.MetersToPixels( mx, my, zoom) 225 | return self.PixelsToTile( px, py) 226 | 227 | def TileBounds(self, tx, ty, zoom): 228 | "Returns bounds of the given tile in EPSG:900913 coordinates" 229 | 230 | minx, miny = self.PixelsToMeters( tx*self.tileSize, ty*self.tileSize, zoom ) 231 | maxx, maxy = self.PixelsToMeters( (tx+1)*self.tileSize, (ty+1)*self.tileSize, zoom ) 232 | return ( minx, miny, maxx, maxy ) 233 | 234 | def TileLatLonBounds(self, tx, ty, zoom ): 235 | "Returns bounds of the given tile in latutude/longitude using WGS84 datum" 236 | 237 | bounds = self.TileBounds( tx, ty, zoom) 238 | minLat, minLon = self.MetersToLatLon(bounds[0], bounds[1]) 239 | maxLat, maxLon = self.MetersToLatLon(bounds[2], bounds[3]) 240 | 241 | return ( minLat, minLon, maxLat, maxLon ) 242 | 243 | def Resolution(self, zoom ): 244 | "Resolution (meters/pixel) for given zoom level (measured at Equator)" 245 | 246 | # return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom) 247 | return self.initialResolution / (2**zoom) 248 | 249 | def ZoomForPixelSize(self, pixelSize ): 250 | "Maximal scaledown zoom of the pyramid closest to the pixelSize." 251 | 252 | for i in range(30): 253 | if pixelSize > self.Resolution(i): 254 | return i-1 if i!=0 else 0 # We don't want to scale up 255 | 256 | def GoogleTile(self, tx, ty, zoom): 257 | "Converts TMS tile coordinates to Google Tile coordinates" 258 | 259 | # coordinate origin is moved from bottom-left to top-left corner of the extent 260 | return tx, (2**zoom - 1) - ty 261 | 262 | def QuadTree(self, tx, ty, zoom ): 263 | "Converts TMS tile coordinates to Microsoft QuadTree" 264 | 265 | quadKey = "" 266 | ty = (2**zoom - 1) - ty 267 | for i in range(zoom, 0, -1): 268 | digit = 0 269 | mask = 1 << (i-1) 270 | if (tx & mask) != 0: 271 | digit += 1 272 | if (ty & mask) != 0: 273 | digit += 2 274 | quadKey += str(digit) 275 | 276 | return quadKey 277 | 278 | #--------------------- 279 | 280 | class GlobalGeodetic(object): 281 | """ 282 | TMS Global Geodetic Profile 283 | --------------------------- 284 | 285 | Functions necessary for generation of global tiles in Plate Carre projection, 286 | EPSG:4326, "unprojected profile". 287 | 288 | Such tiles are compatible with Google Earth (as any other EPSG:4326 rasters) 289 | and you can overlay the tiles on top of OpenLayers base map. 290 | 291 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). 292 | 293 | What coordinate conversions do we need for TMS Global Geodetic tiles? 294 | 295 | Global Geodetic tiles are using geodetic coordinates (latitude,longitude) 296 | directly as planar coordinates XY (it is also called Unprojected or Plate 297 | Carre). We need only scaling to pixel pyramid and cutting to tiles. 298 | Pyramid has on top level two tiles, so it is not square but rectangle. 299 | Area [-180,-90,180,90] is scaled to 512x256 pixels. 300 | TMS has coordinate origin (for pixels and tiles) in bottom-left corner. 301 | Rasters are in EPSG:4326 and therefore are compatible with Google Earth. 302 | 303 | LatLon <-> Pixels <-> Tiles 304 | 305 | WGS84 coordinates Pixels in pyramid Tiles in pyramid 306 | lat/lon XY pixels Z zoom XYZ from TMS 307 | EPSG:4326 308 | .----. ---- 309 | / \ <-> /--------/ <-> TMS 310 | \ / /--------------/ 311 | ----- /--------------------/ 312 | WMS, KML Web Clients, Google Earth TileMapService 313 | """ 314 | 315 | def __init__(self, tileSize = 256): 316 | self.tileSize = tileSize 317 | 318 | def LatLonToPixels(self, lat, lon, zoom): 319 | "Converts lat/lon to pixel coordinates in given zoom of the EPSG:4326 pyramid" 320 | 321 | res = 180 / 256.0 / 2**zoom 322 | px = (180 + lat) / res 323 | py = (90 + lon) / res 324 | return px, py 325 | 326 | def PixelsToTile(self, px, py): 327 | "Returns coordinates of the tile covering region in pixel coordinates" 328 | 329 | tx = int( math.ceil( px / float(self.tileSize) ) - 1 ) 330 | ty = int( math.ceil( py / float(self.tileSize) ) - 1 ) 331 | return tx, ty 332 | 333 | def Resolution(self, zoom ): 334 | "Resolution (arc/pixel) for given zoom level (measured at Equator)" 335 | 336 | return 180 / 256.0 / 2**zoom 337 | #return 180 / float( 1 << (8+zoom) ) 338 | 339 | def TileBounds(tx, ty, zoom): 340 | "Returns bounds of the given tile" 341 | res = 180 / 256.0 / 2**zoom 342 | return ( 343 | tx*256*res - 180, 344 | ty*256*res - 90, 345 | (tx+1)*256*res - 180, 346 | (ty+1)*256*res - 90 347 | ) 348 | 349 | if __name__ == "__main__": 350 | import sys, os 351 | 352 | def Usage(s = ""): 353 | print "Usage: globalmaptiles.py [-profile 'mercator'|'geodetic'] zoomlevel lat lon [latmax lonmax]" 354 | print 355 | if s: 356 | print s 357 | print 358 | print "This utility prints for given WGS84 lat/lon coordinates (or bounding box) the list of tiles" 359 | print "covering specified area. Tiles are in the given 'profile' (default is Google Maps 'mercator')" 360 | print "and in the given pyramid 'zoomlevel'." 361 | print "For each tile several information is printed including bonding box in EPSG:900913 and WGS84." 362 | sys.exit(1) 363 | 364 | profile = 'mercator' 365 | zoomlevel = None 366 | lat, lon, latmax, lonmax = None, None, None, None 367 | boundingbox = False 368 | 369 | argv = sys.argv 370 | i = 1 371 | while i < len(argv): 372 | arg = argv[i] 373 | 374 | if arg == '-profile': 375 | i = i + 1 376 | profile = argv[i] 377 | 378 | if zoomlevel is None: 379 | zoomlevel = int(argv[i]) 380 | elif lat is None: 381 | lat = float(argv[i]) 382 | elif lon is None: 383 | lon = float(argv[i]) 384 | elif latmax is None: 385 | latmax = float(argv[i]) 386 | elif lonmax is None: 387 | lonmax = float(argv[i]) 388 | else: 389 | Usage("ERROR: Too many parameters") 390 | 391 | i = i + 1 392 | 393 | if profile != 'mercator': 394 | Usage("ERROR: Sorry, given profile is not implemented yet.") 395 | 396 | if zoomlevel == None or lat == None or lon == None: 397 | Usage("ERROR: Specify at least 'zoomlevel', 'lat' and 'lon'.") 398 | if latmax is not None and lonmax is None: 399 | Usage("ERROR: Both 'latmax' and 'lonmax' must be given.") 400 | 401 | if latmax != None and lonmax != None: 402 | if latmax < lat: 403 | Usage("ERROR: 'latmax' must be bigger then 'lat'") 404 | if lonmax < lon: 405 | Usage("ERROR: 'lonmax' must be bigger then 'lon'") 406 | boundingbox = (lon, lat, lonmax, latmax) 407 | 408 | tz = zoomlevel 409 | mercator = GlobalMercator() 410 | 411 | mx, my = mercator.LatLonToMeters( lat, lon ) 412 | print "Spherical Mercator (ESPG:900913) coordinates for lat/lon: " 413 | print (mx, my) 414 | tminx, tminy = mercator.MetersToTile( mx, my, tz ) 415 | 416 | if boundingbox: 417 | mx, my = mercator.LatLonToMeters( latmax, lonmax ) 418 | print "Spherical Mercator (ESPG:900913) cooridnate for maxlat/maxlon: " 419 | print (mx, my) 420 | tmaxx, tmaxy = mercator.MetersToTile( mx, my, tz ) 421 | else: 422 | tmaxx, tmaxy = tminx, tminy 423 | 424 | for ty in range(tminy, tmaxy+1): 425 | for tx in range(tminx, tmaxx+1): 426 | tilefilename = "%s/%s/%s" % (tz, tx, ty) 427 | print tilefilename, "( TileMapService: z / x / y )" 428 | 429 | gx, gy = mercator.GoogleTile(tx, ty, tz) 430 | print "\tGoogle:", gx, gy 431 | quadkey = mercator.QuadTree(tx, ty, tz) 432 | print "\tQuadkey:", quadkey, '(',int(quadkey, 4),')' 433 | bounds = mercator.TileBounds( tx, ty, tz) 434 | print 435 | print "\tEPSG:900913 Extent: ", bounds 436 | wgsbounds = mercator.TileLatLonBounds( tx, ty, tz) 437 | print "\tWGS84 Extent:", wgsbounds 438 | print "\tgdalwarp -ts 256 256 -te %s %s %s %s %s %s_%s_%s.tif" % ( 439 | bounds[0], bounds[1], bounds[2], bounds[3], "", tz, tx, ty) 440 | print 441 | --------------------------------------------------------------------------------