source: googlemapmacro/0.11/googlemap.py @ 4664

Last change on this file since 4664 was 4664, checked in by Martin Scharrer, 15 years ago
googlemap.py
Fixed javascript issue with target argument.
  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id HeadURL
  • Property svn:mime-type set to text/x-python
File size: 17.7 KB
Line 
1""" Copyright (c) 2008 Martin Scharrer <martin@scharrer-online.de>
2    $Id: googlemap.py 4664 2008-10-29 23:17:17Z martin_s $
3    $HeadURL: //trac-hacks.org/svn/googlemapmacro/0.11/googlemap.py $
4
5    This is Free Software under the GPL v3!
6""" 
7from genshi.builder import Element,tag
8from StringIO import StringIO
9from trac.core import *
10from trac.util.html import escape,Markup
11from trac.wiki.api import parse_args
12from trac.wiki.formatter import extract_link
13from trac.wiki.macros import WikiMacroBase
14from trac.web.api import IRequestFilter
15from trac.web.chrome import add_script
16from genshi.builder import Element
17from urllib import urlopen,quote_plus
18import md5
19import 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  = {}
30for 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
42function 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
61function 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
107class 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;
498
Note: See TracBrowser for help on using the repository browser.