root/googlemapmacro/0.11/googlemap.py

Revision 4664, 17.7 kB (checked in by martin_s, 8 months ago)
googlemap.py
Fixed javascript issue with target argument.
  • Property svn:mime-type set to text/x-python
  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id HeadURL
Line 
1 """ Copyright (c) 2008 Martin Scharrer <martin@scharrer-online.de>
2     $Id$
3     $HeadURL$
4
5     This is Free Software under the GPL v3!
6 """ 
7 from genshi.builder import Element,tag
8 from StringIO import StringIO
9 from trac.core import *
10 from trac.util.html import escape,Markup
11 from trac.wiki.api import parse_args
12 from trac.wiki.formatter import extract_link
13 from trac.wiki.macros import WikiMacroBase
14 from trac.web.api import IRequestFilter
15 from trac.web.chrome import add_script
16 from genshi.builder import Element
17 from urllib import urlopen,quote_plus
18 import md5
19 import re
20
21 _reWHITESPACES = re.compile(r'\s+')
22 _reCOMMA       = re.compile(r',\s*')
23 _reCOORDS      = re.compile(r'^\d+(?:\.\d*)?[:,]\d+(?:\.\d*)?$')
24 _reDBLQUOTE    = re.compile(r'(?<!\\)"')
25
26 #_allowed_args        = ['center','zoom','size','address']
27 _default_map_types   = ['NORMAL','SATELLITE','HYBRID']
28 _supported_map_types = ['NORMAL','SATELLITE','HYBRID','PHYSICAL']
29 _supported_controls  = {}
30 for control in ( 'LargeMap', 'SmallMap', 'SmallZoom', 'Scale', \
31         'MapType', 'HierarchicalMapType', 'OverviewMap' ):
32     _supported_controls[control.upper()] = control
33
34 _css_units = ('em','ex','px','in','cm','mm','pt','pc')
35
36 _accuracy_to_zoom = (3, 4, 8, 10, 12, 14, 14, 15, 16, 16)
37
38 _javascript_code = """
39 //<![CDATA[
40 // TODO: move this functions to an external file:
41
42 function SetMarkerByCoords(map,lat,lng,letter,link,title,target) {
43     if (!title) { title = link; }
44     var marker = new GMarker( new GLatLng(lat,lng),
45         { title: title, icon: new GIcon (G_DEFAULT_ICON,
46         'http://maps.google.com/mapfiles/marker'
47         + letter + '.png') }
48      );
49     if (link) {
50         GEvent.addListener(marker, "click", function() {
51             if (target) {
52                 window.open(link);
53             } else {
54                 window.location = link;
55             }
56         });
57     }
58     map.addOverlay(marker);
59 }
60
61 function SetMarkerByAddress(map,address,letter,link,title,target,geocoder) {
62     if (!geocoder) {
63         geocoder = new GClientGeocoder();
64     }
65     geocoder.getLatLng(
66       address,
67       function(point) {
68         if (point) {
69           SetMarkerByCoords(map, point.lat(), point.lng(), letter, link, title, target);
70         }
71       }
72     )
73 }
74
75 $(document).ready( function () {
76   if (GBrowserIsCompatible()) {
77     var map = new GMap2(document.getElementById("%(id)s"),{
78     //    size: new GSize(%(width)s, %(height)s),
79         mapTypes: %(types_str)s
80     });
81     %(controls_str)s
82     map.setMapType(G_%(type)s_MAP);
83     if ("%(center)s") {
84         map.setCenter(new GLatLng(%(center)s), %(zoom)s);
85     }
86     var geocoder = new GClientGeocoder();
87     var address = "%(address)s";
88     if (address) {
89     geocoder.getLatLng(
90       address,
91       function(point) {
92         if (!point) {
93           //alert(address + " not found");
94         } else {
95           map.setCenter(point, %(zoom)s);
96         }
97       }
98       )
99     }
100     %(markers_str)s
101 }} );
102
103 $(window).unload( GUnload );
104 //]]>
105 """
106
107 class GoogleMapMacro(WikiMacroBase):
108     implements(IRequestFilter)
109     """ Provides a macro to insert Google Maps(TM) in Wiki pages
110     """
111     nid  = 0
112     dbinit = 0
113
114     def __init__(self):
115         # If geocoding is done on the server side
116         self.geocoding = unicode(self.env.config.get('googlemap', 'geocoding',
117             "client")).lower()
118         # Create DB table if it not exists.
119         # but execute only once.
120         if self.geocoding == 'server' and not GoogleMapMacro.dbinit:
121             self.env.log.debug("Creating DB table (if not already exists).")
122             db = self.env.get_db_cnx()
123             cursor = db.cursor()
124             cursor.execute("""
125                 CREATE TABLE IF NOT EXISTS googlemapmacro (
126                     id char(32) Unique,
127                     lon decimal(10,6),
128                     lat decimal(10,6),
129                     acc decimal(2,0)
130                 );""")
131             db.commit()
132             GoogleMapMacro.dbinit = 1
133
134
135     # IRequestFilter#pre_process_request
136     def pre_process_request(self, req, handler):
137         return handler
138
139
140     # IRequestFilter#post_process_request
141     def post_process_request(self, req, template, data, content_type):
142         # reset macro ID counter at start of each wiki page
143         GoogleMapMacro.nid = 0
144         # Add Google Map JavaScript
145         key = self.env.config.get('googlemap', 'api_key', None)
146         if key:
147             # add_script hack to support external script files:
148             url = r"http://maps.google.com/maps?file=api&v=2&key=%s" % key
149             scriptset = req.chrome.setdefault('scriptset', set())
150             if not url in scriptset:
151                 script = {'href': url, 'type': 'text/javascript'}
152                 req.chrome.setdefault('scripts', []).append(script)
153                 scriptset.add(url)
154         return (template, data, content_type)
155
156     def _strip(self, arg):
157         """Strips spaces and a single pair of double quotes as long there are
158            no unescaped double quotes in the middle.  """
159         arg = unicode(arg).strip()
160         if len(arg) < 2:
161             return arg
162         if arg.startswith('"') and arg.endswith('"') \
163            and not _reDBLQUOTE.match(arg[1:-1]):
164             arg = arg[1:-1]
165         return arg
166
167     def _format_address(self, address):
168         self.env.log.debug("address before = %s" % address)
169         address = self._strip(address).replace(';',',')
170         address = _reWHITESPACES.sub(' ', address)
171         address = _reCOMMA.sub(', ', address)
172         self.env.log.debug("address after  = %s" % address)
173         return address
174
175     def _parse_args(self, args, strict=True, sep = ',', quote = '"', kw = True, min = -1, num = -1):
176         """ parses a comma separated string were the values can include multiple
177         quotes """
178         esc    = 0   # last char was escape char
179         quoted = 0   # inside quote
180         start  = 0   # start of current field
181         pos    = 0   # current position
182         largs  = []
183         kwargs = {}
184
185         def checkkey (arg):
186             import re
187             arg = arg.replace(r'\,', ',')
188             if strict:
189                 m = re.match(r'\s*[a-zA-Z_]\w+=', arg)
190             else:
191                 m = re.match(r'\s*[^=]+=', arg)
192             if m:
193                 kw = arg[:m.end()-1].strip()
194                 if strict:
195                     kw = unicode(kw).encode('utf-8')
196                 kwargs[kw] = self._strip(arg[m.end():])
197             else:
198                 largs.append(self._strip(arg))
199
200         if args:
201             for char in args:
202                 if esc:
203                     esc = 0
204                 elif char == quote:
205                     quoted = not quoted
206                 elif char == '\\':
207                     esc = 1
208                 elif char == sep and not quoted:
209                     checkkey( args[start:pos] )
210                     start = pos + 1
211                 pos = pos + 1
212             checkkey( args[start:] )
213
214         if num > 0:
215             if   len(largs) > num:
216                 largs = largs[0:num]
217             elif len(largs) < num:
218                 min = num
219
220         if min > 0 and min > len(largs):
221             for i in range(len(largs), min):
222                 largs.append(None)
223
224         self.env.log.debug("Parsed Arguments:")
225         self.env.log.debug(largs)
226         self.env.log.debug(kwargs)
227         if kw:
228             return largs, kwargs
229         else:
230             return largs
231
232
233     def _get_coords(self, address):
234         m = md5.new()
235         m.update(address)
236         hash = m.hexdigest()
237
238         db = self.env.get_db_cnx()
239         cursor = db.cursor()
240         #try:
241         cursor.execute("SELECT lon,lat,acc FROM googlemapmacro WHERE id='%s';" % hash)
242         #except:
243         #    pass
244         #else:
245         for row in cursor:
246             if len(row) == 3:
247                 self.env.log.debug("Reusing coordinates from database")
248                 return ( str(row[0]), str(row[1]), str(row[2]) )
249
250         response = None
251         url = r'http://maps.google.com/maps/geo?output=csv&q=' + quote_plus(address)
252         try:
253             response = urlopen(url).read()
254         except:
255             raise TracError("Google Maps could not be contacted to resolve address!");
256         self.env.log.debug("Google geocoding response: '%s'" % response)
257         resp = response.split(',')
258         if len(resp) != 4 or not resp[0] == "200":
259             raise TracError("Given address '%s' couldn't be resolved by Google Maps!" % address);
260         acc, lon, lat = resp[1:4]
261
262         #try:
263         cursor.execute(
264             "INSERT INTO googlemapmacro (id, lon, lat, acc) VALUES ('%s', %s, %s, %s);" %
265             (hash, lon, lat, acc))
266         db.commit()
267         self.env.log.debug("Saving coordinates to database")
268         #except:
269         #    pass
270
271         return (lon, lat, acc)
272
273     def expand_macro(self, formatter, name, content):
274         largs, kwargs = self._parse_args(content)
275         if len(largs) > 0:
276             arg = unicode(largs[0])
277             if _reCOORDS.match(arg):
278                 if not 'center' in kwargs:
279                     kwargs['center'] = arg
280             else:
281                 if not 'address' in kwargs:
282                     kwargs['address'] = arg
283
284         # Check if Google API key is set (if not the Google Map script file
285         # wasn't inserted by `post_process_request` and the map wont load)
286         if not self.env.config.get('googlemap', 'api_key', None):
287             raise TracError("No Google Maps API key given! Tell your web admin to get one at http://code.google.com/apis/maps/signup.html .\n")
288
289         # Use default values if needed
290         zoom = None
291         size = None
292         try:
293             if 'zoom' in kwargs:
294                 zoom = unicode( int( kwargs['zoom'] ) )
295             else:
296                 zoom = unicode( int( self.env.config.get('googlemap', 'default_zoom', "6") ) )
297         except:
298             raise TracError("Invalid value for zoom given! Please provide an integer from 0 to 19.")
299
300         if 'size' in kwargs:
301             size = unicode( kwargs['size'] )
302         else:
303             size = unicode( self.env.config.get('googlemap', 'default_size', "300x300") )
304
305         # Set target for hyperlinked markers
306         target = ""
307         if not 'target' in kwargs:
308             kwargs['target'] = unicode( self.env.config.get('googlemap', 'default_target', "") )
309         if kwargs['target'] in ('new','newwindow','_blank'):
310             target = "newwindow"
311
312         # Get height and width
313         width  = None
314         height = None
315         try:
316             if size.find(':') != -1:
317                 (width,height) = size.lower().split(':')
318                 # Check for correct units:
319                 if    not width[-2:]  in _css_units \
320                    or not height[-2:] in _css_units:
321                        raise TracError("Wrong unit(s)!")
322                 # The rest must be a number:
323                 float( width[:-2]  )
324                 float( height[:-2] )
325             else:
326                 (width,height) = size.lower().split('x')
327                 width  = str( int( width  ) ) + "px"
328                 height = str( int( height ) ) + "px"
329         except:
330             raise TracError("Invalid value for size given! Please provide "
331                             "{width}x{height} in pixels (without unit) or "
332                             "{width}{unit}:{height}{unit} in CSS units (%s)." \
333                                     % ', '.join(_css_units) )
334
335
336         # Correct separator for 'center' argument because comma isn't allowed in
337         # macro arguments
338         center = ""
339         if 'center' in kwargs:
340             center = unicode(kwargs['center']).replace(':',',').strip(' "\'')
341             if not _reCOORDS.match(center):
342                 raise TracError("Invalid center coordinates given!")
343
344         # Format address
345         address = ""
346         if 'address' in kwargs:
347             address = self._format_address(kwargs['address'])
348             if self.geocoding == 'server':
349                 coord = self._get_coords(address)
350                 center = ",".join(coord[0:2])
351                 address = ""
352                 if not 'zoom' in kwargs:
353                     zoom = _accuracy_to_zoom[ int( coord[2] ) ]
354
355         # Internal formatting functions:
356         def gtyp (stype):
357             return "G_%s_MAP" % str(stype)
358         def gcontrol (control):
359             return "map.addControl(new G%sControl());\n" % str(control)
360         def gmarker (lat,lng,letter='',link='',title=''):
361             if not title:
362                 title = link
363             if not letter:
364                 letter = ''
365             else:
366                 letter = str(letter).upper()
367                 if str(letter).startswith('.'):
368                     letter = ''
369                 else:
370                     letter = letter[0]
371             return "SetMarkerByCoords(map,%s,%s,'%s','%s','%s', '%s');\n" \
372                     % (str(lat),str(lng),letter,str(link),str(title),str(target))
373         def gmarkeraddr (address,letter='',link='',title=''):
374             if not title:
375                 title = link
376             if not letter:
377                 letter = ''
378             else:
379                 letter = str(letter).upper()
380                 if str(letter).startswith('.'):
381                     letter = ''
382                 else:
383                     letter = letter[0]
384             return "SetMarkerByAddress(map,'%s','%s','%s','%s','%s',geocoder);\n" \
385                     % (str(address),letter,str(link),str(title),str(target))
386
387         # Set initial map type
388         type = 'NORMAL'
389         types = []
390         types_str = None
391         if 'types' in kwargs:
392             types = unicode(kwargs['types']).upper().split(':')
393             types_str = ','.join(map(gtyp,types))
394             type = types[0]
395
396         if 'type' in kwargs:
397             type = unicode(kwargs['type']).upper()
398             if 'types' in kwargs and not type in types:
399                 types_str += ',' + type
400                 types.insert(0, type)
401             elif not type in _supported_map_types:
402                 type = 'NORMAL'
403             # if types aren't set and a type is set which is supported
404             # but not a default type:
405             if not 'types' in kwargs and type in _supported_map_types and not type in _default_map_types:
406                    # enable type (and all default types):
407                    types = _default_map_types + [type]
408                    types_str = ','.join(map(gtyp,types))
409
410         if types_str:
411             types_str = '[' + types_str + ']'
412         else:
413             types_str = 'G_DEFAULT_MAP_TYPES'
414
415         # Produce controls
416         control_str = ""
417         controls = ['LargeMap','MapType']
418         if 'controls' in kwargs:
419             controls = []
420             for control in unicode(kwargs['controls']).upper().split(':'):
421                 if control in _supported_controls:
422                     controls.append( _supported_controls[control] )
423         controls_str = ''.join(map(gcontrol,controls))
424
425         # Produce markers
426         markers_str = ""
427         if 'markers' in kwargs:
428             markers = []
429             for marker in self._parse_args(unicode(kwargs['markers']), sep='|', kw=False):
430                 location, letter, link, title = self._parse_args(marker,
431                         sep=';', kw=False, num=4 )
432                 if not title:
433                     title = link
434
435                 # Convert wiki to HTML link:
436                 link = extract_link(self.env, formatter.context, link)
437                 if isinstance(link, Element):
438                     link = link.attrib.get('href')
439                 else:
440                     link = ''
441
442                 location = self._format_address(location)
443                 if _reCOORDS.match(location):
444                     coord = location.split(':')
445                     markers.append( gmarker( coord[0], coord[1], letter, link, title ) )
446                 else:
447                     if self.geocoding == 'server':
448                         coord = []
449                         if location == 'center':
450                             if address:
451                                 coord = self._get_coords(address)
452                             else:
453                                 coord = center.split(',')
454                         else:
455                             coord = self._get_coords(location)
456                         markers.append( gmarker( coord[0], coord[1], letter, link, title ) )
457                     else:
458                         if location == 'center':
459                             if address:
460                                 markers.append( gmarkeraddr( address, letter, link, title ) )
461                             else:
462                                 coord = center.split(',')
463                                 markers.append( gmarker( coord[0], coord[1], letter, link, title ) )
464                         else:
465                             markers.append( gmarkeraddr( location, letter, link, title) )
466             markers_str = ''.join( markers )
467
468
469         # Produce unique id for div tag
470         GoogleMapMacro.nid += 1
471         id = "tracgooglemap-%i" % GoogleMapMacro.nid
472
473
474         # put everything in a tidy div
475         html = tag.div(
476                 [
477                     # Initialization script for this map
478                     tag.script ( _javascript_code % { 'id':id,
479                         'center':center,
480                         'zoom':zoom, 'address':address,
481                         'type':type, 'width':width, 'height':height,
482                         'types_str':types_str, 'controls_str':controls_str,
483                         'markers_str':markers_str
484                         },
485                         type = "text/javascript"),
486                     # Canvas for this map
487                     tag.div (
488                         "Google Map is loading ... (JavaScript enabled?)",
489                         id=id,
490                         style= "background-color: rgb(229, 227, 223);" +
491                             "width: %s; height: %s;" % (width,height),
492                         )
493                     ],
494                 class_ = "tracgooglemaps",
495                 );
496
497         return html;
Note: See TracBrowser for help on using the browser.