| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ Copyright (c) 2008-2010 Martin Scharrer <martin@scharrer-online.de> |
|---|
| 3 | $Id: macro.py 17139 2018-04-16 20:14:38Z rjollos $ |
|---|
| 4 | $HeadURL: //trac-hacks.org/svn/googlemapmacro/0.11/tracgooglemap/macro.py $ |
|---|
| 5 | |
|---|
| 6 | This is Free Software under the GPL v3! |
|---|
| 7 | """ |
|---|
| 8 | |
|---|
| 9 | import re |
|---|
| 10 | from urllib import urlopen, quote_plus |
|---|
| 11 | |
|---|
| 12 | from trac.config import Option, IntOption |
|---|
| 13 | from trac.core import TracError, implements |
|---|
| 14 | from trac.env import IEnvironmentSetupParticipant |
|---|
| 15 | from trac.util import md5 |
|---|
| 16 | from trac.util.html import Element, Markup, html as tag |
|---|
| 17 | from trac.wiki.formatter import extract_link |
|---|
| 18 | from trac.wiki.macros import WikiMacroBase |
|---|
| 19 | from trac.web.api import IRequestFilter |
|---|
| 20 | from trac.web.chrome import ( |
|---|
| 21 | ITemplateProvider, add_script, add_link, add_stylesheet) |
|---|
| 22 | |
|---|
| 23 | from tracadvparseargs import parse_args |
|---|
| 24 | |
|---|
| 25 | COUNT = '_googlemapmacro_count' |
|---|
| 26 | |
|---|
| 27 | _reWHITESPACES = re.compile(r'\s+') |
|---|
| 28 | _reCOMMA = re.compile(r',\s*') |
|---|
| 29 | _reCOORDS = re.compile(r'^[+-]?\d+(?:\.\d*)?[:,][+-]?\d+(?:\.\d*)?$') |
|---|
| 30 | _reDBLQUOTE = re.compile(r'(?<!\\)"') |
|---|
| 31 | |
|---|
| 32 | _default_map_types = ['NORMAL', 'SATELLITE', 'HYBRID'] |
|---|
| 33 | _supported_map_types = ['NORMAL', 'SATELLITE', 'HYBRID', 'PHYSICAL'] |
|---|
| 34 | _supported_controls = {} |
|---|
| 35 | for control in ('LargeMap', 'SmallMap', 'SmallZoom', 'Scale', |
|---|
| 36 | 'MapType', 'HierarchicalMapType', 'OverviewMap'): |
|---|
| 37 | _supported_controls[control.upper()] = control |
|---|
| 38 | |
|---|
| 39 | _css_units = ('em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc') |
|---|
| 40 | |
|---|
| 41 | _accuracy_to_zoom = (3, 4, 8, 10, 12, 14, 14, 15, 16, 16) |
|---|
| 42 | |
|---|
| 43 | _javascript_code = """ |
|---|
| 44 | //<![CDATA[ |
|---|
| 45 | |
|---|
| 46 | TracGoogleMap( function (mapdiv,index) { |
|---|
| 47 | var map = new GMap2(mapdiv, { |
|---|
| 48 | // size: new GSize(%(width)s, %(height)s), |
|---|
| 49 | mapTypes: %(types_str)s |
|---|
| 50 | }); |
|---|
| 51 | %(controls_str)s |
|---|
| 52 | map.setMapType(G_%(type)s_MAP); |
|---|
| 53 | if ("%(center)s") { |
|---|
| 54 | map.setCenter(new GLatLng(%(center)s), %(zoom)s); |
|---|
| 55 | } |
|---|
| 56 | var geocoder = new GClientGeocoder(); |
|---|
| 57 | var address = "%(address)s"; |
|---|
| 58 | if (address) { |
|---|
| 59 | geocoder.getLatLng( |
|---|
| 60 | address, |
|---|
| 61 | function(point) { |
|---|
| 62 | if (point) { |
|---|
| 63 | map.setCenter(point, %(zoom)s); |
|---|
| 64 | } |
|---|
| 65 | } |
|---|
| 66 | ) |
|---|
| 67 | } |
|---|
| 68 | %(markers_str)s |
|---|
| 69 | if ("%(directions)s") { |
|---|
| 70 | dirdiv = document.getElementById("tracgooglemap-directions-" + index); |
|---|
| 71 | gdir = new GDirections(map, dirdiv); |
|---|
| 72 | gdir.load("%(directions)s"); |
|---|
| 73 | } |
|---|
| 74 | }, "%(id)s" ); |
|---|
| 75 | |
|---|
| 76 | //]]> |
|---|
| 77 | """ |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | class GoogleMapMacro(WikiMacroBase): |
|---|
| 81 | """ Provides a macro to insert Google Maps(TM) in Wiki pages. |
|---|
| 82 | |
|---|
| 83 | == Description == |
|---|
| 84 | |
|---|
| 85 | Website: http://trac-hacks.org/wiki/GoogleMapMacro |
|---|
| 86 | |
|---|
| 87 | `$Id: macro.py 17139 2018-04-16 20:14:38Z rjollos $` |
|---|
| 88 | |
|---|
| 89 | This macro lets the user insert a full dynamic |
|---|
| 90 | [http://maps.google.com/ Google Map]. Because a lot of javascript is used (by |
|---|
| 91 | Google) a |
|---|
| 92 | [http://local.google.com/support/bin/answer.py?answer=16532&topic=1499 Google Map compatible browser] |
|---|
| 93 | is needed. Newer version of Firefox and MS Internet Explorer are compatible. |
|---|
| 94 | |
|---|
| 95 | For javascript-less static maps use the similar |
|---|
| 96 | [http://trac-hacks.org/wiki/GoogleStaticMapMacro GoogleStaticMapMacro]. |
|---|
| 97 | |
|---|
| 98 | Multiple Google Maps on the same wiki page are actively supported. |
|---|
| 99 | |
|---|
| 100 | == Dependencies == |
|---|
| 101 | The recent version (r480) of this macro needs the AdvParseArgsPlugin in revision |
|---|
| 102 | 4795 or later. |
|---|
| 103 | |
|---|
| 104 | == Configuration == |
|---|
| 105 | |
|---|
| 106 | A different [http://code.google.com/apis/maps/signup.html Google Map API key] is |
|---|
| 107 | needed for every web domain which can be get for free from Google. |
|---|
| 108 | '''Please check if the Google Map API Terms of Use apply for your Trac |
|---|
| 109 | project.''' They do apply IMHO for non-pay open accessible Trac projects. |
|---|
| 110 | |
|---|
| 111 | == Usage == |
|---|
| 112 | The macro knows the following arguments, which can be used in the normal |
|---|
| 113 | `key1=value1,key2=value2,...` syntax. If a value includes one or more comma then |
|---|
| 114 | it must be set in double quotes (`" "`). |
|---|
| 115 | If a key-less value is given it will be taken as `center` coordinates if it's in |
|---|
| 116 | the proper format otherwise it's taken as an `address`. Unknown (or misspelled) |
|---|
| 117 | keys or key-less values except the first are silently ignored. |
|---|
| 118 | |
|---|
| 119 | `address`:: Sets the center of the map to the given address. The value must be |
|---|
| 120 | surrounded by quotes if it includes commas, e.g. `"Street, City, |
|---|
| 121 | Country"`. |
|---|
| 122 | If an `address` but no `zoom` value was given an appropriate value |
|---|
| 123 | will be guessed based on the address accuracy returned by Google. |
|---|
| 124 | At the moment this isn't very accurate but soon |
|---|
| 125 | [http://groups.google.com/group/Google-Maps-API/browse_thread/thread/53c4525e8d01e75d Google might improve] this. |
|---|
| 126 | `center`:: Sets the center of the map to the given coordinates. The format is `{latitude}:{longitude}` or `"{latitude},{longitude}"`. |
|---|
| 127 | `zoom`:: Sets zoom factor. Allowed values are between 0 (whole world) and 19 (single house). Very high zoom values might not be supported by Google Maps for all parts of the world. |
|---|
| 128 | `size`:: The size either in the format `{width}x{height}` as numbers in pixel, e.g.: `300x400` means 300px wide and 400px high or |
|---|
| 129 | in the format `{width}{unit}:{height}{unit}` where unit can be any CSS unit (em, ex, px, in, cm, mm, pt, pc). |
|---|
| 130 | `types` (optional):: Sets the map types selectable by the user, separated by colons ('`:`'). If not given the standard types of Google Maps are used (Normal, Satellite, Hybrid). The following types are available (values are case-insensitive): |
|---|
| 131 | * `normal` Normal street-map |
|---|
| 132 | * `satellite` Satellite picture |
|---|
| 133 | * `hybrid` Satellite picture with streets as overlay |
|---|
| 134 | * `physical` Terrain map |
|---|
| 135 | `type` (optional):: Sets the initial map type. See the `types` argument for the available types. If this argument is not given the first listed type under `types` is used initially. |
|---|
| 136 | `controls` (optional):: Sets the used map controls. Multiple controls can be given, separated by colon ('`:`'). The value is case-insensitive. If not set the controls `MapType` and `LargeMap` are used. |
|---|
| 137 | If set but empty no controls are displayed. The following controls are available (descriptions taken from the [http://code.google.com/apis/maps/documentation/reference.html#GControlImpl Google Map API page]): |
|---|
| 138 | * `LargeMap`: Control with buttons to pan in four directions, and zoom in and zoom out. |
|---|
| 139 | * `SmallMap`: Control with buttons to pan in four directions, and zoom in and zoom out, and a zoom slider. |
|---|
| 140 | * `SmallZoom`: Control with buttons to zoom in and zoom out. |
|---|
| 141 | * `Scale`: Control that displays the map scale. |
|---|
| 142 | * `MapType`: Standard map type control for selecting and switching between supported map types via buttons. |
|---|
| 143 | * `HierarchicalMapType`: Drop-down map type control for switching between supported map types. |
|---|
| 144 | * `OverviewMap`: Collapsible overview mini-map in the corner of the main map for reference location and navigation (through dragging). |
|---|
| 145 | `marker`:: (New) Allows the user to set labeled markers on given location of the map. |
|---|
| 146 | The format for a marker is `{latitude}:{longitude};{Letter};{TracLink};{Title}` or `"{Address}";{Letter};{TracLink};{Title}`. |
|---|
| 147 | If the string 'center' is used instead of an address the center of the map is marked. |
|---|
| 148 | The optional marker letter can be either A-Z or 'o', '.' or empty for a plain marker. |
|---|
| 149 | An optional [TracLinks TracLink] can be given which may be opened in a new window (see `target`) when clicked on the marker. |
|---|
| 150 | An optional marker title can be set which will get displayed when the mouse cursor is over the marker. |
|---|
| 151 | From revision [4801] on multiple markers can be given which replaces the `markers` argument. |
|---|
| 152 | `markers`:: (Old) Can be used to set multiple markers which are are separated using the '`|`' character. |
|---|
| 153 | Optional values can be kept empty, e.g.: `"{Address}";;;My Address` would display the address with the title 'My Address'. |
|---|
| 154 | Trailing semicolons can be skipped. Addresses, links and titles which include either '`,`', '`;`' or '`|`' must be enclosed in double quotes (`" "`). |
|---|
| 155 | `target`:: If set to '`new`' or '`newwindow`' all hyperlinks of the map markers will be opened in a new window (or tab, depending on the browser settings) or in the same window otherwise. |
|---|
| 156 | `from`,`to`:: Request driving directions '`from`'->'`to`'. Multiple `to` addresses are allowed. No `address` or `center` need to be given. |
|---|
| 157 | |
|---|
| 158 | == Examples == |
|---|
| 159 | |
|---|
| 160 | === Using geographic coordinates === |
|---|
| 161 | Please use a colon, not a comma, as separator for the coordinates. |
|---|
| 162 | {{{ |
|---|
| 163 | [[GoogleMap(center=50.0:10.0,zoom=10,size=400x400)]] |
|---|
| 164 | or |
|---|
| 165 | [[GoogleMap("50.0:10.0",zoom=10,size=400x400)]] |
|---|
| 166 | or |
|---|
| 167 | [[GoogleMap(50.0:10.0,zoom=10,size=400x400)]] |
|---|
| 168 | }}} |
|---|
| 169 | === Using an address === |
|---|
| 170 | Please use semicolons, not commas, as separators in the address. |
|---|
| 171 | {{{ |
|---|
| 172 | [[GoogleMap(address="Street, City, Country",zoom=10,size=400x400)]] |
|---|
| 173 | or |
|---|
| 174 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400)]] |
|---|
| 175 | or, if you really want to: |
|---|
| 176 | [[GoogleMap(Street; City; Country,zoom=10,size=400x400)]] |
|---|
| 177 | }}} |
|---|
| 178 | Please note that the address is converted into coordinates by user-side javascript every time the wiki page is loaded. |
|---|
| 179 | If this fails no map will be shown, only an empty gray rectangle. |
|---|
| 180 | |
|---|
| 181 | === Using both === |
|---|
| 182 | If both address and center coordinates are given, then the result depends on the [#config `geocoding` setting]: |
|---|
| 183 | `server`:: The address is resolved on the trac server and the coordinates are completely ignored. |
|---|
| 184 | `client`:: The map is first centered at the given coordinates and then moved to the given address after (and if) it was |
|---|
| 185 | resolved by the client-side !JavaScript code. |
|---|
| 186 | {{{ |
|---|
| 187 | [[GoogleMap(center=50.0:10.0,address="Street, City, Country",zoom=10,size=400x400)]] |
|---|
| 188 | }}} |
|---|
| 189 | |
|---|
| 190 | === Select Map Types === |
|---|
| 191 | To show a map with the standard map types where the satellite map is preselected: |
|---|
| 192 | {{{ |
|---|
| 193 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,type=satellite)]] |
|---|
| 194 | }}} |
|---|
| 195 | |
|---|
| 196 | To only show a satellite map (please note the added '`s`'): |
|---|
| 197 | {{{ |
|---|
| 198 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,types=satellite)]] |
|---|
| 199 | }}} |
|---|
| 200 | |
|---|
| 201 | To show a map with hybrid and satellite map types (in this order) where the satellite map is preselected: |
|---|
| 202 | {{{ |
|---|
| 203 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,types=hybrid:satellite,type=satellite)]] |
|---|
| 204 | }}} |
|---|
| 205 | |
|---|
| 206 | To show a map with hybrid and satellite map types (in this order) where the hybrid map is preselected: |
|---|
| 207 | {{{ |
|---|
| 208 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,types=hybrid:satellite)]] |
|---|
| 209 | or |
|---|
| 210 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,types=hybrid:satellite,type=hybrid)]] |
|---|
| 211 | }}} |
|---|
| 212 | |
|---|
| 213 | === Markers === |
|---|
| 214 | To create three markers: one at the center of the map (A), one at the next street (B) and one at coordinates 10.243,23.343 (C): |
|---|
| 215 | {{{ |
|---|
| 216 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,markers=center;A|"Next street, City, Country";B|10.243:23.343;C)]] |
|---|
| 217 | }}} |
|---|
| 218 | The same with hyperlinked markers: |
|---|
| 219 | {{{ |
|---|
| 220 | [[GoogleMap("Street, City, Country",zoom=10,size=400x400,markers=center;A;wiki:MyWikiPage|"Next street, City, Country";B;ticket:1|10.243:23.343;C;http://www.example.com/)]] |
|---|
| 221 | }}} |
|---|
| 222 | """ |
|---|
| 223 | implements(IRequestFilter, ITemplateProvider, |
|---|
| 224 | IRequestFilter, IEnvironmentSetupParticipant) |
|---|
| 225 | |
|---|
| 226 | geocoding = Option( |
|---|
| 227 | 'googlemap', 'geocoding', 'client', |
|---|
| 228 | """Which side is handling the geocoding: either "server" or |
|---|
| 229 | "client" (default). |
|---|
| 230 | """) |
|---|
| 231 | |
|---|
| 232 | api_key = Option( |
|---|
| 233 | 'googlemap', 'api_key', '', |
|---|
| 234 | """Google Map API key. Available from |
|---|
| 235 | http://code.google.com/apis/maps/signup.html . |
|---|
| 236 | """) |
|---|
| 237 | |
|---|
| 238 | default_zoom = IntOption( |
|---|
| 239 | 'googlemap', 'default_zoom', '6', |
|---|
| 240 | "Default map zoom used if no zoom specified by the user (default: 6)") |
|---|
| 241 | |
|---|
| 242 | default_size = Option( |
|---|
| 243 | 'googlemap', 'default_size', '300x300', |
|---|
| 244 | """Default map size (width x height, in pixel without units) used |
|---|
| 245 | if no size specified by the user (default: 300x300) |
|---|
| 246 | """) |
|---|
| 247 | |
|---|
| 248 | default_target = Option( |
|---|
| 249 | 'googlemap', 'default_target', '', |
|---|
| 250 | """Default target for hyperlinked markers. Use "_blank" to open |
|---|
| 251 | target in new window. (Default: "") |
|---|
| 252 | """) |
|---|
| 253 | |
|---|
| 254 | def __init__(self): |
|---|
| 255 | self.geocoding_server = self.geocoding.lower() == "server" |
|---|
| 256 | |
|---|
| 257 | def _create_db_table(self, db=None, commit=True): |
|---|
| 258 | """ Create DB table if it not exists. """ |
|---|
| 259 | if self.geocoding_server: |
|---|
| 260 | self.log.debug("Creating DB table (if not already exists).") |
|---|
| 261 | |
|---|
| 262 | db = db or self.env.get_db_cnx() |
|---|
| 263 | cursor = db.cursor() |
|---|
| 264 | cursor.execute(""" |
|---|
| 265 | CREATE TABLE IF NOT EXISTS googlemapmacro ( |
|---|
| 266 | id char(32) Unique, |
|---|
| 267 | lon decimal(10,6), |
|---|
| 268 | lat decimal(10,6), |
|---|
| 269 | acc decimal(2,0) |
|---|
| 270 | );""") |
|---|
| 271 | if commit: |
|---|
| 272 | db.commit() |
|---|
| 273 | return |
|---|
| 274 | |
|---|
| 275 | def environment_created(self): |
|---|
| 276 | self._create_db_table() |
|---|
| 277 | return |
|---|
| 278 | |
|---|
| 279 | def environment_needs_upgrade(self, db): |
|---|
| 280 | if not self.geocoding_server: |
|---|
| 281 | return False |
|---|
| 282 | cursor = db.cursor() |
|---|
| 283 | try: |
|---|
| 284 | cursor.execute("SELECT count(*) FROM googlemapmacro;") |
|---|
| 285 | cursor.fetchone() |
|---|
| 286 | except: |
|---|
| 287 | return True |
|---|
| 288 | return False |
|---|
| 289 | |
|---|
| 290 | def upgrade_environment(self, db): |
|---|
| 291 | self._create_db_table(db, False) |
|---|
| 292 | return |
|---|
| 293 | |
|---|
| 294 | # ITemplateProvider#get_htdocs_dirs |
|---|
| 295 | |
|---|
| 296 | def get_htdocs_dirs(self): |
|---|
| 297 | from pkg_resources import resource_filename |
|---|
| 298 | return [('googlemap', resource_filename(__name__, 'htdocs'))] |
|---|
| 299 | |
|---|
| 300 | # ITemplateProvider#get_templates_dirs |
|---|
| 301 | def get_templates_dirs(self): |
|---|
| 302 | return [] |
|---|
| 303 | |
|---|
| 304 | # IRequestFilter#pre_process_request |
|---|
| 305 | |
|---|
| 306 | def pre_process_request(self, req, handler): |
|---|
| 307 | return handler |
|---|
| 308 | |
|---|
| 309 | # IRequestFilter#post_process_request |
|---|
| 310 | |
|---|
| 311 | def post_process_request(self, req, template, data, content_type): |
|---|
| 312 | # Add Google Map API key using a link tag: |
|---|
| 313 | if self.api_key: |
|---|
| 314 | add_link(req, rel='google-key', href='', |
|---|
| 315 | title=self.api_key, classname='google-key') |
|---|
| 316 | add_stylesheet(req, 'googlemap/tracgooglemap.css') |
|---|
| 317 | add_script(req, 'googlemap/tracgooglemap.js') |
|---|
| 318 | return (template, data, content_type) |
|---|
| 319 | |
|---|
| 320 | def _strip(self, arg): |
|---|
| 321 | """Strips spaces and a single pair of double quotes as long there are |
|---|
| 322 | no unescaped double quotes in the middle. """ |
|---|
| 323 | arg = unicode(arg).strip() |
|---|
| 324 | if len(arg) < 2: |
|---|
| 325 | return arg |
|---|
| 326 | if arg.startswith('"') and arg.endswith('"') \ |
|---|
| 327 | and not _reDBLQUOTE.match(arg[1:-1]): |
|---|
| 328 | arg = arg[1:-1] |
|---|
| 329 | return arg |
|---|
| 330 | |
|---|
| 331 | def _format_address(self, address): |
|---|
| 332 | address = self._strip(address).replace(';', ',') |
|---|
| 333 | address = _reWHITESPACES.sub(' ', address) |
|---|
| 334 | address = _reCOMMA.sub(', ', address) |
|---|
| 335 | return address |
|---|
| 336 | |
|---|
| 337 | def _get_coords(self, address): |
|---|
| 338 | m = md5() |
|---|
| 339 | m.update(address) |
|---|
| 340 | hash = m.hexdigest() |
|---|
| 341 | |
|---|
| 342 | db = self.env.get_db_cnx() |
|---|
| 343 | cursor = db.cursor() |
|---|
| 344 | cursor.execute( |
|---|
| 345 | "SELECT lon,lat,acc FROM googlemapmacro WHERE id='%s';" % hash) |
|---|
| 346 | for row in cursor: |
|---|
| 347 | if len(row) == 3: |
|---|
| 348 | self.log.debug("Reusing coordinates from database") |
|---|
| 349 | return (str(row[0]), str(row[1]), str(row[2])) |
|---|
| 350 | |
|---|
| 351 | response = None |
|---|
| 352 | url = r'http://maps.google.com/maps/geo?output=csv&q=' + \ |
|---|
| 353 | quote_plus(address) |
|---|
| 354 | try: |
|---|
| 355 | response = urlopen(url).read() |
|---|
| 356 | except: |
|---|
| 357 | raise TracError( |
|---|
| 358 | "Google Maps could not be contacted to resolve address!") |
|---|
| 359 | self.log.debug("Google geocoding response: '%s'", response) |
|---|
| 360 | resp = response.split(',') |
|---|
| 361 | if len(resp) != 4 or not resp[0] == "200": |
|---|
| 362 | raise TracError( |
|---|
| 363 | "Given address '%s' couldn't be resolved by Google Maps!" |
|---|
| 364 | % address) |
|---|
| 365 | acc, lon, lat = resp[1:4] |
|---|
| 366 | |
|---|
| 367 | cursor.execute(""" |
|---|
| 368 | INSERT INTO googlemapmacro (id, lon, lat, acc) |
|---|
| 369 | VALUES ('%s', %s, %s, %s); |
|---|
| 370 | """, (hash, lon, lat, acc)) |
|---|
| 371 | db.commit() |
|---|
| 372 | self.env.log.debug("Saving coordinates to database") |
|---|
| 373 | |
|---|
| 374 | return (lon, lat, acc) |
|---|
| 375 | |
|---|
| 376 | def expand_macro(self, formatter, name, content, args=None): |
|---|
| 377 | content = content.replace('\n', ',') |
|---|
| 378 | largs, kwargs = parse_args(content, multi=['marker', 'to']) |
|---|
| 379 | if len(largs) > 0: |
|---|
| 380 | arg = unicode(largs[0]) |
|---|
| 381 | if _reCOORDS.match(arg): |
|---|
| 382 | if 'center' not in kwargs: |
|---|
| 383 | kwargs['center'] = arg |
|---|
| 384 | else: |
|---|
| 385 | if 'address' not in kwargs: |
|---|
| 386 | kwargs['address'] = arg |
|---|
| 387 | if 'from' in kwargs and 'address' not in kwargs and \ |
|---|
| 388 | 'center' not in kwargs: |
|---|
| 389 | arg = unicode(kwargs['from']) |
|---|
| 390 | if _reCOORDS.match(arg): |
|---|
| 391 | if 'center' not in kwargs: |
|---|
| 392 | kwargs['center'] = arg |
|---|
| 393 | else: |
|---|
| 394 | if 'address' not in kwargs: |
|---|
| 395 | kwargs['address'] = arg |
|---|
| 396 | |
|---|
| 397 | # Check if Google API key is set (if not the Google Map script file |
|---|
| 398 | # wasn't inserted by `post_process_request` and the map wont load) |
|---|
| 399 | if not self.api_key: |
|---|
| 400 | raise TracError( |
|---|
| 401 | "No Google Maps API key given! Tell your web admin to " |
|---|
| 402 | "get one at http://code.google.com/apis/maps/signup.html.\n") |
|---|
| 403 | |
|---|
| 404 | # Use default values if needed |
|---|
| 405 | zoom = None |
|---|
| 406 | size = None |
|---|
| 407 | try: |
|---|
| 408 | if 'zoom' in kwargs: |
|---|
| 409 | zoom = unicode(int(kwargs['zoom'])) |
|---|
| 410 | else: |
|---|
| 411 | zoom = unicode(self.default_zoom) |
|---|
| 412 | except: |
|---|
| 413 | raise TracError( |
|---|
| 414 | "Invalid value for zoom given! Please provide an integer " |
|---|
| 415 | "from 0 to 19.") |
|---|
| 416 | |
|---|
| 417 | if 'size' in kwargs: |
|---|
| 418 | size = unicode(kwargs['size']) |
|---|
| 419 | else: |
|---|
| 420 | size = unicode(self.default_size) |
|---|
| 421 | |
|---|
| 422 | # Set target for hyperlinked markers |
|---|
| 423 | target = "" |
|---|
| 424 | if 'target' not in kwargs: |
|---|
| 425 | kwargs['target'] = self.default_target |
|---|
| 426 | if kwargs['target'] in ('new', 'newwindow', '_blank'): |
|---|
| 427 | target = "newwindow" |
|---|
| 428 | |
|---|
| 429 | # Get height and width |
|---|
| 430 | width = None |
|---|
| 431 | height = None |
|---|
| 432 | try: |
|---|
| 433 | if size.find(':') != -1: |
|---|
| 434 | (width, height) = size.lower().split(':') |
|---|
| 435 | # Check for correct units: |
|---|
| 436 | if not width[-2:] in _css_units \ |
|---|
| 437 | or not height[-2:] in _css_units: |
|---|
| 438 | raise TracError("Wrong unit(s)!") |
|---|
| 439 | # The rest must be a number: |
|---|
| 440 | float(width[:-2]) |
|---|
| 441 | float(height[:-2]) |
|---|
| 442 | else: |
|---|
| 443 | (width, height) = size.lower().split('x') |
|---|
| 444 | width = str(int(width)) + "px" |
|---|
| 445 | height = str(int(height)) + "px" |
|---|
| 446 | except: |
|---|
| 447 | raise TracError("Invalid value for size given! Please provide " |
|---|
| 448 | "{width}x{height} in pixels (without unit) or " |
|---|
| 449 | "{width}{unit}:{height}{unit} in CSS units (%s)." |
|---|
| 450 | % ', '.join(_css_units)) |
|---|
| 451 | |
|---|
| 452 | # Correct separator for 'center' argument because comma isn't |
|---|
| 453 | # allowed in macro arguments |
|---|
| 454 | center = "" |
|---|
| 455 | if 'center' in kwargs: |
|---|
| 456 | center = unicode(kwargs['center']).replace(':', ',').strip(' "\'') |
|---|
| 457 | if not _reCOORDS.match(center): |
|---|
| 458 | raise TracError("Invalid center coordinates given!") |
|---|
| 459 | |
|---|
| 460 | # Format address |
|---|
| 461 | address = "" |
|---|
| 462 | if 'address' in kwargs: |
|---|
| 463 | address = self._format_address(kwargs['address']) |
|---|
| 464 | if self.geocoding_server: |
|---|
| 465 | coord = self._get_coords(address) |
|---|
| 466 | center = ",".join(coord[0:2]) |
|---|
| 467 | address = "" |
|---|
| 468 | if 'zoom' not in kwargs: |
|---|
| 469 | zoom = _accuracy_to_zoom[int(coord[2])] |
|---|
| 470 | |
|---|
| 471 | # Internal formatting functions: |
|---|
| 472 | def gtyp(stype): |
|---|
| 473 | return "G_%s_MAP" % str(stype) |
|---|
| 474 | |
|---|
| 475 | def gcontrol(control): |
|---|
| 476 | return "map.addControl(new G%sControl());\n" % str(control) |
|---|
| 477 | |
|---|
| 478 | def gmarker(lat, lng, letter='', link='', title=''): |
|---|
| 479 | if not title: |
|---|
| 480 | title = link |
|---|
| 481 | if not letter: |
|---|
| 482 | letter = '' |
|---|
| 483 | else: |
|---|
| 484 | letter = str(letter).upper() |
|---|
| 485 | if str(letter).startswith('.'): |
|---|
| 486 | letter = '' |
|---|
| 487 | else: |
|---|
| 488 | letter = letter[0] |
|---|
| 489 | return ("SetMarkerByCoords(map,%s,%s,'%s','%s','%s', '%s');\n" |
|---|
| 490 | % (lat, lng, letter, link, title, target)) |
|---|
| 491 | |
|---|
| 492 | def gmarkeraddr(address, letter='', link='', title=''): |
|---|
| 493 | if not title: |
|---|
| 494 | title = link |
|---|
| 495 | if not letter: |
|---|
| 496 | letter = '' |
|---|
| 497 | else: |
|---|
| 498 | letter = str(letter).upper() |
|---|
| 499 | if str(letter).startswith('.'): |
|---|
| 500 | letter = '' |
|---|
| 501 | else: |
|---|
| 502 | letter = letter[0] |
|---|
| 503 | return ( |
|---|
| 504 | "SetMarkerByAddress(map,'%s','%s','%s','%s','%s',geocoder);\n" |
|---|
| 505 | % (address, letter, link, title, target)) |
|---|
| 506 | |
|---|
| 507 | # Set initial map type |
|---|
| 508 | type = 'NORMAL' |
|---|
| 509 | types = [] |
|---|
| 510 | types_str = None |
|---|
| 511 | if 'types' in kwargs: |
|---|
| 512 | types = unicode(kwargs['types']).upper().split(':') |
|---|
| 513 | types_str = ','.join(map(gtyp, types)) |
|---|
| 514 | type = types[0] |
|---|
| 515 | |
|---|
| 516 | if 'type' in kwargs: |
|---|
| 517 | type = unicode(kwargs['type']).upper() |
|---|
| 518 | if 'types' in kwargs and type not in types: |
|---|
| 519 | types_str += ',' + type |
|---|
| 520 | types.insert(0, type) |
|---|
| 521 | elif type not in _supported_map_types: |
|---|
| 522 | type = 'NORMAL' |
|---|
| 523 | # if types aren't set and a type is set which is supported |
|---|
| 524 | # but not a default type: |
|---|
| 525 | if 'types' not in kwargs and type in _supported_map_types and \ |
|---|
| 526 | type not in _default_map_types: |
|---|
| 527 | # enable type (and all default types): |
|---|
| 528 | types = _default_map_types + [type] |
|---|
| 529 | types_str = ','.join(map(gtyp, types)) |
|---|
| 530 | |
|---|
| 531 | if types_str: |
|---|
| 532 | types_str = '[' + types_str + ']' |
|---|
| 533 | else: |
|---|
| 534 | types_str = 'G_DEFAULT_MAP_TYPES' |
|---|
| 535 | |
|---|
| 536 | # Produce controls |
|---|
| 537 | controls = ['LargeMap', 'MapType'] |
|---|
| 538 | if 'controls' in kwargs: |
|---|
| 539 | controls = [] |
|---|
| 540 | for control in unicode(kwargs['controls']).upper().split(':'): |
|---|
| 541 | if control in _supported_controls: |
|---|
| 542 | controls.append(_supported_controls[control]) |
|---|
| 543 | controls_str = ''.join(map(gcontrol, controls)) |
|---|
| 544 | |
|---|
| 545 | # Produce markers |
|---|
| 546 | markers_str = "" |
|---|
| 547 | if 'marker' not in kwargs: |
|---|
| 548 | kwargs['marker'] = [] |
|---|
| 549 | if 'markers' in kwargs: |
|---|
| 550 | kwargs['marker'].extend( |
|---|
| 551 | parse_args(unicode(kwargs['markers']), delim='|', |
|---|
| 552 | listonly=True)) |
|---|
| 553 | if kwargs['marker']: |
|---|
| 554 | markers = [] |
|---|
| 555 | for marker in kwargs['marker']: |
|---|
| 556 | location, letter, link, title = \ |
|---|
| 557 | parse_args(marker, delim=';', listonly=True, minlen=4)[:4] |
|---|
| 558 | if not title: |
|---|
| 559 | title = link |
|---|
| 560 | |
|---|
| 561 | # Convert wiki to HTML link: |
|---|
| 562 | link = extract_link(self.env, formatter.context, link) |
|---|
| 563 | if isinstance(link, Element): |
|---|
| 564 | link = link.attrib.get('href') |
|---|
| 565 | else: |
|---|
| 566 | link = '' |
|---|
| 567 | |
|---|
| 568 | location = self._format_address(location) |
|---|
| 569 | if _reCOORDS.match(location): |
|---|
| 570 | coord = location.split(':') |
|---|
| 571 | markers.append( |
|---|
| 572 | gmarker(coord[0], coord[1], letter, link, title)) |
|---|
| 573 | else: |
|---|
| 574 | if self.geocoding_server: |
|---|
| 575 | coord = [] |
|---|
| 576 | if location == 'center': |
|---|
| 577 | if address: |
|---|
| 578 | coord = self._get_coords(address) |
|---|
| 579 | else: |
|---|
| 580 | coord = center.split(',') |
|---|
| 581 | else: |
|---|
| 582 | coord = self._get_coords(location) |
|---|
| 583 | markers.append( |
|---|
| 584 | gmarker(coord[0], coord[1], letter, link, title)) |
|---|
| 585 | else: |
|---|
| 586 | if location == 'center': |
|---|
| 587 | if address: |
|---|
| 588 | markers.append(gmarkeraddr( |
|---|
| 589 | address, letter, link, title)) |
|---|
| 590 | else: |
|---|
| 591 | coord = center.split(',') |
|---|
| 592 | markers.append( |
|---|
| 593 | gmarker(coord[0], coord[1], letter, |
|---|
| 594 | link, title)) |
|---|
| 595 | else: |
|---|
| 596 | markers.append(gmarkeraddr( |
|---|
| 597 | location, letter, link, title)) |
|---|
| 598 | markers_str = ''.join(markers) |
|---|
| 599 | |
|---|
| 600 | # Get macro count from request object |
|---|
| 601 | req = formatter.req |
|---|
| 602 | count = getattr(req, COUNT, 0) |
|---|
| 603 | id = 'tracgooglemap-%s' % count |
|---|
| 604 | setattr(req, COUNT, count + 1) |
|---|
| 605 | |
|---|
| 606 | # Canvas for this map |
|---|
| 607 | mapdiv = tag.div( |
|---|
| 608 | "Google Map is loading ... (JavaScript enabled?)", |
|---|
| 609 | id=id, |
|---|
| 610 | style="width: %s; height: %s;" % (width, height), |
|---|
| 611 | class_="tracgooglemap" |
|---|
| 612 | ) |
|---|
| 613 | |
|---|
| 614 | if 'from' in kwargs and 'to' in kwargs: |
|---|
| 615 | directions = "from: %s to: %s" % ( |
|---|
| 616 | kwargs['from'], ' to: '.join(list(kwargs['to']))) |
|---|
| 617 | mapnmore = tag.table( |
|---|
| 618 | tag.tr( |
|---|
| 619 | tag.td( |
|---|
| 620 | tag.div("", |
|---|
| 621 | class_='tracgooglemap-directions', |
|---|
| 622 | id='tracgooglemap-directions-%s' % count |
|---|
| 623 | ), |
|---|
| 624 | style="vertical-align:top;", |
|---|
| 625 | ), |
|---|
| 626 | tag.td( |
|---|
| 627 | mapdiv, |
|---|
| 628 | style="vertical-align:top;", |
|---|
| 629 | ) |
|---|
| 630 | ), |
|---|
| 631 | class_='tracgooglemaps' |
|---|
| 632 | ) |
|---|
| 633 | |
|---|
| 634 | else: |
|---|
| 635 | directions = "" |
|---|
| 636 | mapnmore = mapdiv |
|---|
| 637 | |
|---|
| 638 | # put everything in a tidy div |
|---|
| 639 | html = tag.div( |
|---|
| 640 | [ |
|---|
| 641 | # Initialization script for this map |
|---|
| 642 | tag.script(Markup(_javascript_code % { |
|---|
| 643 | 'id': id, |
|---|
| 644 | 'center': center, |
|---|
| 645 | 'zoom': zoom, 'address': address, |
|---|
| 646 | 'type': type, 'width': width, 'height': height, |
|---|
| 647 | 'types_str': types_str, 'controls_str': controls_str, |
|---|
| 648 | 'markers_str': markers_str, 'directions': directions, |
|---|
| 649 | }), type='text/javascript'), |
|---|
| 650 | mapnmore |
|---|
| 651 | ], |
|---|
| 652 | class_='tracgooglemap-parent' |
|---|
| 653 | ) |
|---|
| 654 | |
|---|
| 655 | return html |
|---|