#! /usr/bin/env pike string cache = getenv( "HOME" ) + "/.gmap/"; string format = "PNG", save_as = "full.png"; mapping formats = ([ "PNG":1, "JPG":"JPEG", "JPEG":1, "GIF":1, "BMP":1 ]); int verbose = 0, debug = 0, compose = 1, width = 1, height = 1, zoom = 18; int main( int argc, array(string) argv ) { foreach( Getopt.find_all_options( argv, ({ ({ "!compose", Getopt.NO_ARG, ({ "-n", "--no-compose" })}), ({ "output", Getopt.HAS_ARG, ({ "-o", "--output" })}), ({ "height", Getopt.HAS_ARG, ({ "-h", "--height" })}), ({ "width", Getopt.HAS_ARG, ({ "-w", "--width" })}), ({ "zoom", Getopt.HAS_ARG, ({ "-z", "--zoom" })}), ({ "help", Getopt.NO_ARG, ({ "--help" })}), ({ "debug", Getopt.NO_ARG, ({ "-d", "--debug" })}), ({ "verbose", Getopt.NO_ARG, ({ "-v", "--verbose" })}) })), array opt ) switch( opt[0] ) { case "!compose": compose = 0; break; case "output": save_as = opt[1]; string ext = upper_case(reverse( (reverse(save_as)/".")[0] )); if( formats[ext] == 1 ) format = ext; else format = formats[ext] || format; break; case "height": height = (int)opt[1] || height; break; case "width": width = (int)opt[1] || width; break; case "zoom": zoom = (int)opt[1]; break; case "verbose": verbose = 1; break; case "debug": debug = 1; break; case "help": write(#"Usage: gmap [options] lat long Options: -n --no-compose : do not compose a large image; only populate the tile cache -o --output=... : save result as this file name (picking format by extension) -z --zoom=... : choose zoom level (0: whole earth in one tile; 19: max zoom) -w --width=... : how many tiles wide the download grid is -h --height=... : how many tiles tall the download grid is -h --help : show this help -d --debug : save links to tiles named by grid position and tile url -v --verbose : list those names to stdout during download / cache lookup "); exit( 0 ); } argv = Getopt.get_args( argv ); if( sizeof( argv ) != 3 ) { werror( "Try gmap --help for options" ); exit( 1 ); } Position pos = Position( (float)argv[1], (float)argv[2], zoom, "degrees" ); download_grid( walk_perimeter( pos, width, height ) ); } // downloads a map grid into separate files and pastes them together into // a single image too. All files are put in the present working directory. void download_grid( array(array(string)) urls ) { mkdir( cache ); int w = sizeof( urls[0] ), h = sizeof( urls ); int s = (int)max( Math.log10( (float)w ), Math.log10( (float)h ) ); string filename_format = "%0"+s+"d-%0"+s+"d %s.png"; Image.Image tile, large = compose && Image.Image( w*256, h*256 ); for( int y = 0; y < h; y++ ) for( int x = 0; x < w; x++ ) { string pos = urls[y][x]; string url = "http://kh"+((x+y)%4)+".google.com/kh?n=0&v=0&t="+pos; string filename = sprintf( filename_format, y+1, x+1, pos ); string cached = cache + pos + ".png"; if( verbose && !file_stat( cached ) ) werror( "Get " ); if( verbose ) write( "%s\n", filename ); string image = file_stat( cached ) ? Stdio.read_file( cached ) : Protocols.HTTP.get_url_data( url ); if( !sizeof( image ) ) { werror( "Tile unavailable at this zoom level (%s).\n", pos ); continue; } if( !file_stat( cached ) ) Stdio.write_file( cached, image ); if( debug ) { rm( filename ); if( catch( System.hardlink( cached, filename ) ) ) System.symlink( cached, filename ); } tile = Image.ANY.decode( image ); compose && large->paste( tile, x*256, y*256 ); } compose && Stdio.write_file( save_as, Image[format]->encode( large ) ); } // return a @[w] by @[h] url grid centered around @[pos] array(array(string)) walk_perimeter( Position pos, int w, int h ) { //werror( "%d\n", pos->zoom ); string url = pos->get_url( pos->zoom ); Position tile = Position( url ); // lose precision to tile center position array(array(string)) grid = allocate( h, allocate( w ) ); float wx = 2*Math.pi / pow( 2, tile->zoom ); float wy = Math.pi / pow( 2, tile->zoom ); int xmin = -( w==1 ? 0 : w&1 ? w - (pos->x < tile->x) : w ) / 2; int ymin = -( h==1 ? 0 : h&1 ? h - (pos->y < tile->y) : h ) / 2; for( int y = 0; y < h; y++ ) for( int x = 0; x < w; x++ ) grid[h-y-1][x] = Position(tile->y + (y+ymin)*wy, tile->x + (x+xmin)*wx)->get_url( tile->zoom ); return grid; } float rad_to_deg( float rad ) { return rad / Math.pi * 180; } float deg_to_rad( float deg ) { return deg * Math.pi / 180; } class Position { int zoom; string url; float x, y; // radians, y not compensated for mercator projection // @decl create( string url ) // a position url // @decl create( float x, float y ) // lat, long in radians -- lat not being mercator compensated // @decl create( float x, float y, int zoom, "degrees" ) // lat, long in degrees, zoom level mixed create( string|float _y, float|void _x, int|void _zoom, string|void unit, string|void _url ) { if( stringp( _y ) ) return set_from_url( _y ); if( unit == "degrees" ) return set_from_latlong( _y, _x, _zoom ); set( _y, _x, zero_type( _zoom ) ? 18 : _zoom, _url ); } constant radius = 0.5; float mercator_to_y( float rad ) { return radius/2.0 * log( (1.0 + sin( rad )) / (1.0 - sin( rad )) ); } float y_to_mercator( float y ) { return (Math.pi/2) - (2.0 * atan( exp(-1.0 * y / radius) )); } static this_program set( float _y, float _x, int _zoom, string|void _url ) { y = _y; x = _x; zoom = _zoom; url = _url; //werror( "Set %O:%O %O %O:%O %O\n", y, x, zoom, @get_latlong(), url ); return this_program; } this_program set_from_latlong( float lat, float long, int|void _zoom ) { return set( mercator_to_y( deg_to_rad( lat ) ), deg_to_rad( long ), _zoom ); } array(float) get_latlong() { return ({ rad_to_deg( y_to_mercator( y ) ), rad_to_deg( x ) }); } this_program set_from_url( string url ) { string tmp = url; float xmin = -Math.pi, xmax = 3*Math.pi, xmid; float ymin = -Math.pi/2, ymax = 3*Math.pi/2, ymid; int z = sizeof( url )-1, quadrant; while( 1 ) { xmid = (xmax + xmin) / 2; ymid = (ymax + ymin) / 2; if( !sscanf( tmp, "%c%s", quadrant, tmp ) ) return set( ymid, xmid, z, url ); switch( quadrant ) { case 'q': /* nw */ ymin = ymid; xmax = xmid; break; case 'r': /* ne */ ymin = ymid; xmin = xmid; break; case 's': /* se */ ymax = ymid; xmin = xmid; break; case 't': /* sw */ ymax = ymid; xmax = xmid; break; default: throw( sprintf( "Illegal quadrant %c (%s)!", quadrant, url ) ); } } } string get_url( int|void _zoom ) { if( zero_type( _zoom ) && zero_type( zoom ) ) zoom = 18; else zoom = _zoom; float xmin = -Math.pi, xmax = Math.pi, xmid; float ymin = -Math.pi/2, ymax = Math.pi/2, ymid; url = "t"; for( int i = 0; i < zoom; i++ ) { xmid = (xmax + xmin) / 2; ymid = (ymax + ymin) / 2; if( y > ymid ) { ymin = ymid; // north if( x < xmid ) { url += "q"; xmax = xmid; // -west } else { url += "r"; xmin = xmid; // -east } } else { ymax = ymid; // south if( x < xmid ) { url += "t"; xmax = xmid; // -west } else { url += "s"; xmin = xmid; // -east } } } return url; } }