%let name=dallas_attractions; filename odsout '.'; /* These are the markers you want to plot on the map. */ data my_data; infile datalines pad truncover; input lat_deg lon_deg description $ 24-200; datalines; 32.7741900 -96.8005812 Kay Bailey Hutchison Convention Center 32.7814082 -96.7983795 Giant Eyeball 32.7997811 -96.8242754 Medieval Times Dinner & Tournament 32.7480688 -96.8342992 Bishop Arts District, Dallas 32.7404454 -96.8164119 Dallas Zoo 32.6833088 -96.8539620 Bahama Beach Waterpark 32.9725207 -96.8351744 Cavanaugh Flight Museum 32.8425106 -96.8354361 Frontiers of Flight Museum 32.7833034 -96.7824529 Deep Ellum (entertainment district) 32.7875099 -96.8020077 Dallas Museum of Art 32.7879772 -96.7996428 Crow Museum of Asian Art 32.7753575 -96.8092998 Reunion Tower 32.9506549 -96.8178791 Public School 972 (restaurant) 32.7861198 -96.7885123 Traveling Man Walking Tall (sculpture) 32.7805197 -96.7576361 The Texas Woofus (statue) 32.7799480 -96.8086848 Sixth Floor Museum (JFK museum) 32.7799706 -96.8226336 Margaret Hunt Hill Bridge 32.7868238 -96.8067896 Perot Museum of Nature and Science 32.7904682 -96.8105244 American Airlines Center 32.8362286 -96.7147953 White Rock Lake Park 32.7433792 -96.8261089 The Texas Theatre 32.7765111 -96.8014082 Cattle Drive Sculptures 32.7786231 -96.8074496 Old Red Museum 32.8852096 -96.9012325 Zero Gravity Thrill Amusement Park 32.8411329 -96.7784442 George W. Bush Presidential Center 32.7769857 -96.7603774 Children's Aquarium at Fair Park 32.8701273 -96.7675594 Bowl & Barrel 32.8130267 -96.7535768 Cock & Bull pub 32.7698087 -96.8000342 Alamo Drafthouse Cinema 32.8120742 -96.7612878 Lakewood Landing bar 32.7844176 -96.7840898 Off the Record (funky bar and record store) 32.7825956 -96.7852216 Adair's Saloon 32.8107021 -96.8105333 Round-Up Saloon 32.7893593 -96.8023472 Yoga at Klyde Warren Park 32.7833470 -96.8053983 Dallas World Aquarium 32.7764168 -96.8344033 Graffiti Fabrication Yard 32.7680518 -96.7896523 Lee Harvey's 32.7479214 -96.8289124 Dude, Sweet Chocolate 32.7685851 -96.7982077 Gilley's (mechanical bull & food) 32.9546320 -96.8254178 The Magic Time Machine restaurant 32.7427169 -96.8330021 El Ranchito (Tex-Mex restaurant) 32.8055799 -96.7951043 McKinney Avenue Trolley tour (free) 32.8125006 -96.7704200 Good Records (vinyl & indie performances) 32.6806641 -96.8647234 Delta Charlie's (dinner in a helicopter?) 32.9288878 -96.8225913 Galleria Mall (and ice skating) ; run; proc sort data=my_data out=my_data; by descending lat_deg; run; proc sql noprint; select unique lat_deg into :sgf_lat from my_data where index(description,'Kay Bailey Hutchison Convention Center')^=0; select unique lon_deg into :sgf_lon from my_data where index(description,'Kay Bailey Hutchison Convention Center')^=0; quit; run; data my_data; set my_data; sgf_lat=&sgf_lat; sgf_lon=&sgf_lon; distance=geodist(sgf_lat, sgf_lon, lat_deg, lon_deg,'DM'); run; /* Download & prepare the map tiles */ /* Here is the zoom-level, and the min/max x/y numbers of the desired OSM tiles ... (Alternatively, it would be a simple matter to programmatically determine the tiles needed to display a given list of lat/long point data.) You can convert a lat/long & zoom-level to the OSM x/y tile number using the following equation: --------- %let zoomlvl=12; data foo; lon_deg=-96.8005812; lat_deg=32.7741900; lat_rad=(atan(1)/45)*lat_deg; n=2**&zoomlvl; x = ((lon_deg + 180) / 360) * n; y = (1 - (log(tan(lat_rad) + (1/cos(lat_rad))) / constant('pi') )) / 2 * n; run; proc print data=foo; run; x, y = 946.624 1652.93 */ %let zoomlvl=12; %let min_x=945; %let max_x=948; %let min_y=1651; /* top of map */ %let max_y=1653; /* bottom of map */ /* The url for one of the map image tile-servers... http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames */ %let tileserv=c.tile.openstreetmap.org; %include '../data_vault/proxy.sas'; /* I %include a file that sets the my_proxy macro variable. You won't be able to use my proxy, therefore set your own macro variable, which will be something like the following: %let my_proxy=http://yourproxy.com:80; */ /* Programmatically read in those map images, and save them to my hard drive so I can later annotate them (it would be nice if annotate could just refer to them without actually downloading them ... if anyone figures out how to do that, let me know! :) (This bit of code was adapted from some code Jim Metcalf was using to download a spreadsheet from the web. Note that your 'proxy' url will be different, for your own Intranet.)) */ %macro download_map(zoom, x, y); filename osm_pic url "http://&tileserv./&zoom./&x./&y..png" proxy="&my_proxy"; data _null_; n=-1; infile osm_pic recfm=s nbyte=n length=len _infile_=tmp; input; file "opnsta_&zoom._&x._&y..png" recfm=n; put tmp $varying32767. len; run; %mend; /* loop through all the desired map tile x/y numbers, and call the macro for each */ /* If you're re-running the same map several times, you probably want to comment this out, after the first time, since the png files are already downloaded. */ /* data _null_; do loop_x=&min_x to &max_x; do loop_y=&min_y to &max_y; call execute('%download_map('|| &zoomlvl ||','|| loop_x ||','|| loop_y ||');'); call execute('run;'); end; end; run; */ /* Create a blank rectangular map area of the same proportions as your images. Use the OpenStreetMap tile numbers. */ data blank_map; x=&min_x; y=&min_y; output; x=&max_x+1; y=&min_y; output; x=&max_x+1; y=&max_y+1; output; x=&min_x; y=&max_y+1; output; run; /* Create an annotate data set to 'stitch' the images together. */ data anno_maps; length function style $8; xsys='2'; ysys='2'; when='b'; style='fit'; do loop_x=&min_x to &max_x; do loop_y=&min_y to &max_y; imgpath="opnsta_&zoomlvl._"||trim(left(loop_x))||"_"||trim(left(loop_y))||".png"; /* the y-coordinates might seem "reversed", but you will be "flipping" them later */ function='move'; x=loop_x; y=loop_y+1; output; function='image'; x=loop_x+1; y=loop_y; output; end; end; run; /* Convert the long & lat degrees to x & y in the OSM tile-based coordinate system, using equations from: http://wiki.openstreetmap.org/wiki/Tilenames SAS doesn't have a sec() function, so I used 1/cos(value) */ data anno_markers; set my_data; lon_rad=(atan(1)/45)*lon_deg; lat_rad=(atan(1)/45)*lat_deg; n=2**&zoomlvl; x=((lon_deg + 180)/360)*n; y=(1-(log(tan(lat_rad)+(1/cos(lat_rad)))/constant('pi')))/2*n; run; /* Annotate a marker at each location */ data anno_markers; set anno_markers; length function $8 color $12 style $20; xsys='2'; ysys='2'; hsys='3'; when='a'; if index(description,"Kay Bailey Hutchison Convention Center")^=0 then do; function='pie'; rotate=360; size=1.0; style='psolid'; color="yellow"; output; style='pempty'; color="red"; output; size=.6; output; end; else do; function='pie'; angle=90-20; rotate=20*2; size=2.0; style='psolid'; color="cyan"; output; style='pempty'; color="black"; output; end; run; /* Add html drilldown to the markers */ data anno_markers; set anno_markers; length html $500; if style='pempty' then html= 'title='||quote(trim(left(description))||'0d'x|| trim(left(put(distance,comma5.1)))||' miles from convention center')|| ' href='||quote('https://www.google.com/search?&q='||trim(left(translate(description,'+','&')))||' Dallas'); run; /* Flip the y-value, since SAS/GRAPH's coordinate system's origin is at bottom/left, and OpenStreetMaps' coordinate system's origin is at the top/left */ data blank_map; set blank_map; y=-y; run; data anno_maps; set anno_maps; y=-y; run; data anno_markers; set anno_markers; y=-y; run; data foo; zoomlvl=&zoomlvl; min_x=&min_x; min_y=&min_y; max_x=&max_x; max_y=&max_y; run; proc sql noprint; /* gplot axis length, in inches (each tile is 256x256 pixels, and output is 96ppi) */ select ((max_x-min_x+1)*256)/96 into :x_length from foo; select ((max_y-min_y+1)*256)/96 into :y_length from foo; /* size for entire page ... needs to be bigger, to hold titles & axis text, etc */ select (((max_x-min_x+1)*256))+25 into :x_pixels from foo; select (((max_y-min_y+1)*256))+25 into :y_pixels from foo; quit; run; /* Annotate a "Dallas" logo. */ /* coordinates of bottom_left corner of the logo */ %let xc=58; %let yc=80; data anno_logo; length function $8 color $12 style $35 text $100; xsys='3'; ysys='3'; hsys='3'; when='a'; /* light background */ x=&xc; y=&yc; function='move'; output; x=&xc+33; y=&yc+14; function='bar'; style='solid'; color="cxfcfcfc"; output; x=&xc; y=&yc; function='move'; output; x=&xc+33; y=&yc+14; function='bar'; style='empty'; line=0; color="graybb"; output; line=.; /* DALLAS text */ function='label'; position='6'; style="albany amt/bold"; x=&xc+2.5; y=&yc+10.5; color="cx4458b0"; size=9.0; text='DALLAS'; output; /* fill in the middle of the 'D' in Dallas */ function='label'; position='6'; style="albany amt/unicode"; x=&xc+2.5; y=&yc+10.5+1.0; color="cx4458b0"; size=9.0; text='25a0'x; output; /* Put a star in the 'D' in Dallas */ function='label'; position='6'; style="wingdings"; x=&xc+2.5; y=&yc+10.5-.5; color="cxfcfcfc"; size=6.0; text='ab'x; output; /* More text below DALLAS */ function='label'; position='6'; style="albany amt/bold"; x=&xc+2.5+.5; y=&yc+3.2; color="gray77"; size=2.9; text='Big Things Happen Here!'; output; run; /* combine the markers and text ... into anno_stuff */ data anno_stuff; set anno_logo anno_markers; run; goptions device=png; goptions xpixels=&x_pixels ypixels=&y_pixels; goptions border; ODS LISTING CLOSE; ODS HTML path=odsout body="&name..htm" (title="Dallas - places to go") options(pagebreak='no') style=htmlblue; goptions htitle=18pt htext=12pt; /* the 4 corner points will be basically 'invisible' */ symbol1 value=point interpol=none color=black; title; footnote; /* Draw the plot & map, and suppress all the axis & refline stuff that makes it look like a gplot (those things help you see what's going on while you're first setting the map up, but for the final map you'll probably want to remove them (as shown below), so it just looks like a map & not a plot. (style=0 would also suppress the border/axis, if you don't want that) */ axis1 color=white label=none offset=(0,0) length=&y_length in major=none minor=none value=none /*style=0*/; axis2 color=white label=none offset=(0,0) length=&x_length in major=none minor=none value=none /*style=0*/; proc gplot data=blank_map anno=anno_maps; note link='http://www.openstreetmap.org/copyright' move=(70,1.2) height=8pt 'a9a0'x color=blue underlin=1 "OpenStreetMap" c=black underlin=0 " contributors"; plot y*x / anno=anno_stuff vaxis=axis1 haxis=axis2 des='' name="&name"; run; ods html anchor='table'; proc sort data=my_data out=my_data; by distance description; run; data my_data; set my_data; length link $300 href $300; href='href='||quote('https://www.google.com/search?&q='||trim(left(translate(description,'+','&')))||' Dallas'); link = '' || htmlencode(trim(description)) || ''; run; title; proc print data=my_data noobs split=' ' label style(data)={font_size=12pt borderwidth=7 bordercolor=white} style(header)={background=white bordercolor=white font_size=12pt color=gray33} style(table)={bordercolor=white} ; format distance comma8.1; label distance='Distance (miles)'; label link='Attraction'; var distance link; run; quit; ODS HTML CLOSE; ODS LISTING;