PageRenderTime 496ms CodeModel.GetById 110ms app.highlight 189ms RepoModel.GetById 96ms app.codeStats 38ms

/static/js/gmaptools.backbone.js

https://github.com/gotomypc/gmaptools
JavaScript | 638 lines | 525 code | 50 blank | 63 comment | 32 complexity | 22d363b6f216991c19fd7580ff2d970b MD5 | raw file
  1/*!
  2 * gmaptools backbone JavaScript Library v0.1
  3 * http://.../
  4 *
  5 * Copyright 2012, Janos Gyerik
  6 * http://.../license
  7 *
  8 * Date: Sun Sep 16 23:00:33 CEST 2012
  9 */
 10
 11
 12// the basic namespace
 13// TODO: put in app.js
 14var App = window.App = {};
 15
 16_.templateSettings = { interpolate: /\{\{(.+?)\}\}/g };
 17
 18// classes
 19// TODO: put in app/*.js
 20
 21App.MapInfoDetails = Backbone.View.extend({
 22    initialize: function() {
 23        this.model.on('change', this.render, this);
 24    },
 25
 26    template: _.template($('#mapinfo-details-template').html()),
 27
 28    render: function() {
 29        this.$el.html(this.template(this.model.toJSON()));
 30        return this;
 31    }
 32});
 33
 34App.MapInfoQuickView = Backbone.View.extend({
 35    initialize: function() {
 36        this.model.on('change', this.render, this);
 37    },
 38
 39    template: _.template($('#mapinfo-quickview-template').html()),
 40
 41    render: function() {
 42        this.$el.html(this.template(this.model.toJSON()));
 43        return this;
 44    }
 45});
 46
 47App.LocationFactory = Backbone.Collection.extend({
 48    getLocation: function(lat, lon) {
 49        return new google.maps.LatLng(lat, lon);
 50    }
 51});
 52
 53App.MarkerFactory = Backbone.Collection.extend({
 54    initialize: function(map) {
 55        this.map = map;
 56    },
 57    getMarker: function(pos, icon) {
 58        var marker = new google.maps.Marker({
 59            position: pos,
 60            map: this.map,
 61            title: pos.toString(),
 62            icon: icon
 63        });
 64        //markers.push(marker);
 65        return marker;
 66    }
 67});
 68
 69App.MarkerImageFactory = Backbone.Collection.extend({
 70    baseurl: 'http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons',
 71    initialize: function() {
 72        this.presets = {
 73            search: this.baseurl + '/red-dot.png',
 74            latlon: this.baseurl + '/blue-dot.png',
 75            localSearch: this.baseurl + '/yellow-dot.png',
 76            geocode: this.baseurl + '/orange-dot.png'
 77        };
 78    },
 79    getPresetMarkerImage: function(name) {
 80        var src = this.presets[name];
 81        return new google.maps.MarkerImage(src);
 82    },
 83    getCustomMarkerImage: function(src, options) {
 84        if (options.size) {
 85            return new google.maps.MarkerImage(src, null, null, null, new google.maps.Size(options.size, options.size));
 86        }
 87        else {
 88            return new google.maps.MarkerImage(src);
 89        }
 90    }
 91});
 92
 93App.Place = Backbone.Model.extend({
 94    defaults: {
 95        lat: null,
 96        lon: null,
 97        name: '',
 98        address: '',
 99        types: [],
100        marker: null,
101        icon: '',  // derived from .marker
102        typesStr: ''  // derived from .types
103    },
104    initialize: function() {
105        this.on('change:types', this.onTypesChanged, this);
106        this.on('change:marker', this.onMarkerChanged, this);
107        this.trigger('change:types');
108        this.trigger('change:marker');
109    },
110    onTypesChanged: function() {
111        var types = this.get('types');
112        var typesStr = _.map(types, function(item) {
113            return '<span class="badge">' + item + '</span>';
114        }).join(' ');
115        this.set({typesStr: typesStr});
116    },
117    onMarkerChanged: function() {
118        this.set({icon: this.get('marker').icon.url});
119    },
120    destroy: function() {
121        this.get('marker').setMap(null);
122    }
123});
124
125App.Places = Backbone.Collection.extend({
126    model: App.Place,
127    initialize: function() {
128        // TODO: I don't know why, but without this line
129        // the trigger in PlaceView does not work...
130        this.on('add', this.onChange, this);
131    },
132    onChange: function() {
133    }
134});
135
136App.PlaceView = Backbone.View.extend({
137    className: 'place',
138    template: _.template($('#mapinfo-places-item-template').html()),
139    events: {
140        'click .goto': 'goto',
141        'click a.destroy': 'clear'
142    },
143    goto: function() {
144        var lat = this.model.get('lat');
145        var lon = this.model.get('lon');
146        this.map.trigger('gotoLatLon', lat, lon);
147    },
148    initialize: function() {
149        this.$el.html(this.template(this.model.toJSON()));
150        return this;
151    },
152    clear: function() {
153        this.model.destroy();
154        this.remove();
155    }
156});
157
158App.PlacesView = Backbone.View.extend({
159    initialize: function(options) {
160        this.map = options.map;
161        this.collection.on('add', this.add, this);
162    },
163    add: function(place) {
164        var view = new App.PlaceView({model: place});
165        view.map = this.map;
166        this.$el.prepend(view.render().el);
167        App.placesTab.activate();
168    }
169});
170
171App.MapController = Backbone.Model.extend({
172    defaults: {
173        status: 'N.A.',
174        lat: 35.68112175616982,
175        lon: 139.76703710980564,
176        zoom: 14,
177        address: 'N.A.',
178        sw_latitude: 'N.A.',
179        sw_longitude: 'N.A.',
180        ne_latitude: 'N.A.',
181        ne_longitude: 'N.A.'
182    },
183    initialize: function(places) {
184        this.places = places;
185
186        // initialize helper factory objects
187        this.locationFactory = new App.LocationFactory();
188        this.markerImageFactory = new App.MarkerImageFactory();
189
190        // initialize google map objects
191        if (google.loader.ClientLocation) {
192            var lat = google.loader.ClientLocation.latitude;
193            var lon = google.loader.ClientLocation.longitude;
194            this.defaults.lat = lat;
195            this.defaults.lon = lon;
196            this.set({lat: lat, lon: lon});
197        }
198        var center = this.locationFactory.getLocation(this.get('lat'), this.get('lon'));
199        var options = {
200            zoom: this.get('zoom'),
201            center: center,
202            mapTypeId: google.maps.MapTypeId.ROADMAP
203        };
204        this.map = new google.maps.Map(document.getElementById('map_canvas'), options);
205        this.placesService = new google.maps.places.PlacesService(this.map);
206        this.geocoder = new google.maps.Geocoder();
207        this.markerFactory = new App.MarkerFactory(this.map);
208
209        // make sure *this* is bound to *this* in the map event handlers
210        _.bindAll(this, 'centerChanged', 'zoomChanged', 'dragend');
211
212        // google maps event handlers
213        google.maps.event.addListener(this.map, 'center_changed', this.centerChanged);
214        google.maps.event.addListener(this.map, 'zoom_changed', this.zoomChanged);
215        google.maps.event.addListener(this.map, 'dragend', this.dragend);
216
217        // event handlers for latlon tool
218        this.on('getCurrentLatLon', this.getCurrentLatLon, this);
219        this.on('gotoHome', this.gotoHome, this);
220        this.on('gotoLatLon', this.gotoLatLon, this);
221        this.on('dropPin', this.dropPin, this);
222
223        // event handlers for local search tool
224        this.on('localSearch', this.localSearch, this);
225        this.on('geocode', this.geocode, this);
226    },
227
228    centerChanged: function() {
229        var center = this.map.getCenter();
230        var params = {
231            lat: center.lat(),
232            lon: center.lng()
233        };
234        var bounds = this.map.getBounds();
235        if (bounds) {
236            var sw = bounds.getSouthWest();
237            var ne = bounds.getNorthEast();
238            _.extend(params, {
239                sw_latitude: sw.lat(),
240                sw_longitude: sw.lng(),
241                ne_latitude: ne.lat(),
242                ne_longitude: ne.lng()
243            });
244        }
245        this.set(params);
246    },
247    zoomChanged: function() {
248        this.set({zoom: this.map.getZoom()});
249    },
250    dragend: function() {
251        this.updateAddress();
252    },
253
254    getCurrentLatLon: function(callback) {
255        callback(this.get('lat'), this.get('lon'));
256    },
257    gotoHome: function() {
258        this.gotoLatLon(this.defaults.lat, this.defaults.lon);
259    },
260    gotoLatLon: function(lat, lon) {
261        this.set({lat: lat, lon: lon});
262        this.map.setCenter(this.locationFactory.getLocation(lat, lon));
263        this.map.panTo(this.locationFactory.getLocation(lat, lon));
264        this.updateAddress();
265    },
266    dropPin: function(lat, lon) {
267        if (!(lat || lon)) {
268            lat = this.get('lat');
269            lon = this.get('lon');
270        }
271        var pos = this.locationFactory.getLocation(lat, lon);
272        var markerImage = this.markerImageFactory.getPresetMarkerImage('latlon');
273        this.markerFactory.getMarker(pos, markerImage);
274    },
275    localSearch: function(keyword) {
276        var request = {
277            location: this.map.getCenter(),
278            rankBy: google.maps.places.RankBy.DISTANCE,
279            keyword: keyword
280        };
281        // note: could not make this work with a var callback = function ...
282        // had to create this.localSearchCallback to have proper
283        // bind of *this* using _.bindAll
284        // also, I couldn't get it to work with _.bind either...
285        _.bindAll(this, 'localSearchCallback');
286        this.placesService.search(request, this.localSearchCallback);
287    },
288    localSearchCallback: function(results, status) {
289        this.set({status: status});
290        if (status === google.maps.places.PlacesServiceStatus.OK) {
291            // this.errors.hide();
292            for (var i = results.length - 1; i >= 0; --i) {
293                var place = results[i];
294                var markerImage = this.markerImageFactory.getCustomMarkerImage(place.icon, {size: 25});
295                var marker = this.markerFactory.getMarker(place.geometry.location, markerImage);
296                this.places.add({
297                    lat: place.geometry.location.lat(),
298                    lon: place.geometry.location.lng(),
299                    name: place.name,
300                    address: place.vicinity,
301                    types: place.types,
302                    marker: marker
303                });
304            }
305        }
306        else {
307            // this.errors.clear();
308            // this.errors.append('Local search failed');
309            // this.errors.show();
310        }
311    },
312    geocode: function(address) {
313        var request = {
314            address: address,
315            partialmatch: true
316        };
317        // note: could not make this work with a var callback = function ...
318        // had to create this.geocodeCallback to have proper
319        // bind of *this* using _.bindAll
320        // also, I couldn't get it to work with _.bind either...
321        _.bindAll(this, 'geocodeCallback');
322        this.geocoder.geocode(request, this.geocodeCallback);
323    },
324    geocodeCallback: function(results, status) {
325        this.updateStatusFromGeocodeResults(results, status);
326        var typeToWords = function(name) { return name.replace(/_/g, ' '); };
327        if (status === google.maps.GeocoderStatus.OK) {
328            for (var i = results.length - 1; i >= 0; --i) {
329                var result = results[i];
330                if (i === 0) {
331                    this.map.fitBounds(result.geometry.viewport);
332                    this.map.setCenter(result.geometry.location);
333                }
334                var markerImage = this.markerImageFactory.getPresetMarkerImage('geocode');
335                var marker = this.markerFactory.getMarker(result.geometry.location, markerImage);
336                this.places.add({
337                    lat: result.geometry.location.lat(),
338                    lon: result.geometry.location.lng(),
339                    address: result.formatted_address,
340                    types: _.map(result.types, typeToWords),
341                    marker: marker
342                });
343            }
344        }
345    },
346    updateStatusFromGeocodeResults: function(results, status) {
347        this.set({status: status});
348        if (status === google.maps.GeocoderStatus.OK) {
349            // this.errors.hide();
350            for (var i = 0; i < results.length; ++i) {
351                var result = results[i];
352                this.set({address: result.formatted_address});
353                break;
354            }
355        } else {
356            // this.errors.clear();
357            // this.errors.append('Local search failed');
358            // this.errors.show();
359        }
360    },
361    updateAddress: function() {
362        var request = {
363            latLng: this.map.getCenter()
364        };
365        // note: could not make this work with a var callback = function ...
366        // had to create this.geocodeCallback to have proper
367        // bind of *this* using _.bindAll
368        // also, I couldn't get it to work with _.bind either...
369        _.bindAll(this, 'updateStatusFromGeocodeResults');
370        this.geocoder.geocode(request, this.updateStatusFromGeocodeResults);
371    }
372});
373
374App.Tool = Backbone.View.extend({
375    activate: function() {
376        var id = this.$el.attr('id');
377        var anchor = $('a[href=#' + id + ']');
378        anchor.tab('show');
379        if (this.fieldToFocus) {
380            this.fieldToFocus.focus();
381        }
382    }
383});
384
385App.InfoTab = App.Tool.extend();
386App.PlacesTab = App.Tool.extend();
387
388App.LatlonTool = App.Tool.extend({
389    el: $('#latlon-tool'),
390    initialize: function(options) {
391        this.lat = this.$('.lat');
392        this.lon = this.$('.lon');
393        this.map = options.map;
394        this.$('.features').popover({
395            content: $('#features-latlon').html(),
396            placement: 'bottom',
397            trigger: 'hover'
398        });
399    },
400    fieldToFocus: this.$('.lat'),
401    events: {
402        'click .btn-goto': 'gotoLatLon',
403        'click .btn-pin':  'dropPin',
404        'click .btn-here': 'getCurrentLatLon',
405        'click .btn-home': 'gotoHome',
406        'keypress .lat': 'onEnter',
407        'keypress .lon': 'onEnter'
408    },
409    gotoLatLon: function() {
410        var lat = this.lat.val();
411        var lon = this.lon.val();
412        if (lat && lon) {
413            this.map.trigger('gotoLatLon', lat, lon);
414        }
415    },
416    dropPin: function() {
417        var lat = this.lat.val();
418        var lon = this.lon.val();
419        if (lat && lon) {
420            this.map.trigger('gotoLatLon', lat, lon);
421            this.map.trigger('dropPin', lat, lon);
422        }
423        else if (!(lat || lon)) {
424            this.map.trigger('dropPin');
425            this.getCurrentLatLon();
426        }
427    },
428    getCurrentLatLon: function() {
429        var lat = this.lat;
430        var lon = this.lon;
431        var callback = function(lat_, lon_) {
432            lat.val(lat_);
433            lon.val(lon_);
434        };
435        this.map.trigger('getCurrentLatLon', callback);
436    },
437    gotoHome: function() {
438        this.map.trigger('gotoHome');
439    },
440    onEnter: function(e) {
441        if (e.keyCode === 13) {
442            e.preventDefault();
443            this.gotoLatLon();
444        }
445    }
446});
447
448App.LocalSearchTool = App.Tool.extend({
449    el: $('#localsearch-tool'),
450    initialize: function(options) {
451        this.keyword = this.$('.keyword');
452        this.map = options.map;
453        this.$('.features').popover({
454            content: $('#features-localsearch').html(),
455            placement: 'bottom',
456            trigger: 'hover'
457        });
458    },
459    fieldToFocus: this.$('.keyword'),
460    events: {
461        'click .btn-local': 'localSearch',
462        'keypress .keyword': 'onEnter'
463    },
464    localSearch: function() {
465        var keyword = this.keyword.val();
466        if (keyword) {
467            this.map.trigger('localSearch', keyword);
468        }
469    },
470    onEnter: function(e) {
471        if (e.keyCode === 13) {
472            e.preventDefault();
473            this.localSearch();
474        }
475    }
476});
477
478App.GeocodeTool = App.Tool.extend({
479    el: $('#geocode-tool'),
480    initialize: function(options) {
481        this.address = this.$('.address');
482        this.map = options.map;
483        this.$('.features').popover({
484            content: $('#features-geocode').html(),
485            placement: 'bottom',
486            trigger: 'hover'
487        });
488    },
489    fieldToFocus: this.$('.address'),
490    events: {
491        'click .btn-geocode': 'geocode',
492        'keypress .address': 'onEnter'
493    },
494    geocode: function() {
495        var address = this.address.val();
496        if (address) {
497            this.map.trigger('geocode', address);
498        }
499    },
500    onEnter: function(e) {
501        if (e.keyCode === 13) {
502            e.preventDefault();
503            this.geocode();
504        }
505    }
506});
507
508App.Toolbar = Backbone.View.extend({
509    el: $('#toolbar'),
510    events: {
511        'click a[href="#latlon-tool"]': 'openLatlonTool',
512        'click a[href="#localsearch-tool"]': 'openLocalSearchTool',
513        'click a[href="#geocode-tool"]': 'openGeocodeTool'
514    },
515    openLatlonTool: function() {
516        App.router.openLatlonTool();
517    },
518    openLocalSearchTool: function() {
519        App.router.openLocalSearchTool();
520    },
521    openGeocodeTool: function() {
522        App.router.openGeocodeTool();
523    }
524});
525
526App.Router = Backbone.Router.extend({
527    routes: {
528        'tools/latlon': 'activateLatlon',
529        'tools/localSearch': 'activateLocalSearch',
530        'tools/geocode': 'activateGeocode',
531        '*placeholder': 'activateLatlon'
532    },
533    activateTool: function(tool) {
534        tool.activate();
535    },
536    activateLatlon: function() {
537        this.activateTool(App.latlonTool);
538    },
539    activateLocalSearch: function() {
540        this.activateTool(App.localSearchTool);
541    },
542    activateGeocode: function() {
543        this.activateTool(App.geocodeTool);
544    },
545    openLatlonTool: function() {
546        this.navigate('tools/latlon', {trigger: true});
547    },
548    openLocalSearchTool: function() {
549        this.navigate('tools/localSearch', {trigger: true});
550    },
551    openGeocodeTool: function() {
552        this.navigate('tools/geocode', {trigger: true});
553    }
554});
555
556App.OfflineView = Backbone.View.extend({
557    el: $('#main-content'),
558
559    template: _.template($('#offline-template').html()),
560
561    render: function() {
562        this.$el.html(this.template());
563        return this;
564    }
565});
566
567function onGoogleMapsReady() {
568    // instances
569    // TODO: put in setup.js
570    App.places = new App.Places();
571    App.mapController = new App.MapController(App.places);
572    App.latlonTool = new App.LatlonTool({map: App.mapController});
573    App.localSearchTool = new App.LocalSearchTool({map: App.mapController});
574    App.geocodeTool = new App.GeocodeTool({map: App.mapController});
575    App.toolbar = new App.Toolbar();
576
577    App.infoTab = new App.InfoTab({el: '#mapinfo-details'});
578    App.placesTab = new App.PlacesTab({el: '#mapinfo-places'});
579
580    App.detailedstats = new App.MapInfoDetails({
581        el: $('#mapinfo-details'),
582        model: App.mapController
583    });
584
585    App.quickstats = new App.MapInfoQuickView({
586        el: $('#mapinfo-quickview'),
587        model: App.mapController
588    });
589
590    App.placesView = new App.PlacesView({
591        el: $('#mapinfo-places'),
592        collection: App.places,
593        map: App.mapController
594    });
595
596    App.router = new App.Router();
597
598    // initialize the Backbone router
599    Backbone.history.start();
600
601    // debugging
602    //App.latlonTool.activate();
603    //App.latlonTool.getCurrentLatLon();
604    //App.latlonTool.lat.val(2);
605    //App.latlonTool.lon.val(3);
606    //App.latlonTool.gotoLatLon();
607    //App.latlonTool.dropPin();
608    //App.latlonTool.gotoHome();
609
610    //App.placesTab.activate();
611    //App.localSearchTool.activate();
612    //App.localSearchTool.keyword.val('pizza');
613    //App.localSearchTool.localSearch();
614
615    //App.placesTab.activate();
616    //App.geocodeTool.activate();
617    //App.geocodeTool.address.val('6 Rue Gassendi, 75014 Paris, France');
618    //App.geocodeTool.geocode();
619
620    // this is to force all views to render
621    App.mapController.trigger('change');
622}
623
624function offlineMode() {
625    var offlineView = new App.OfflineView();
626    offlineView.render();
627}
628
629$(function() {
630    if (typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.event !== 'undefined') {
631        google.maps.event.addDomListener(window, 'load', onGoogleMapsReady);
632    }
633    else {
634        offlineMode();
635    }
636});
637
638// eof