/* Functionality to display google map complete with marker management.
 * 
 * Written by Andy Wardley  abw@wardley.org for Completely Retail.
 * December 2008, updated February and March 2009
 */


var DEFAULT = {
    element:      'map',
    width:        '500',
    height:       '400',
    zoom:          8,
    map_type:      G_NORMAL_MAP,
    cell_width:    20,
    cell_height:   30,
    info_width:    250,
    inset_width:   150,
    inset_height:  120,
    latitude:     '54.1517',
    longitude:    '-4.50205',
    cluster:       0,
    zoom:          5, 
    zoom_in:       15,
    group_icon:    'mixed_group',
    group_name:    'Schemes',
//    poi_zoom:      13,
    pause:         500,
    overlay:       { },
    icon_styles: {
        'default': {
            width: 40,
            height: 40,
            anchor_x: 0,
            anchor_y: 40,
            window_x: 20,
            window_y: 12,
            label_x: 0,
            label_y: -21
        },
        'square': {
            width: 35,
            height: 35,
            anchor_x: 0,
            anchor_y: 35,
            window_x: 26,
            window_y: 10
        },
        'small_dot': {
            width: 20,
            height: 20,
            anchor_x: 6,
            anchor_y: 20,
            window_x: 12,
            window_y: 9
        },
        'smaller_dot': {
            width: 14,
            height: 14,
            anchor_x: 5,
            anchor_y: 14,
            window_x: 9,
            window_y: 9
        },
        'large_dot': {
            width: 32,
            height: 32,
            anchor_x: 12,
            anchor_y: 32,
            window_x: 23,
            window_y: 11, 
            label_x: -12,
            label_y: -21
        },
        'poi': {
            width: 32,
            height: 32,
            anchor_x: 12,
            anchor_y: 32,
            window_x: 22,
            window_y: 14
        },
        'poi_scheme': {
            width: 25,
            height: 25,
            anchor_x: 10,
            anchor_y: 25,
            window_x: 19,
            window_y: 7
        }
    },
    icons: {
        HS_TL: {
            style:  'star',
            image:  '/images/map/star_yellow.png',
            shadow: '/images/map/star_shadow.png'
        },
        RW_plain: {
            style:  'smaller_dot',
            image:  '/images/map/smaller_dot_blue.png',
            shadow: '/images/map/smaller_dot_shadow.png'
        },
        RW_brand: {
            style:  'square',
            image:  '/images/map/square_blue.png',
            shadow: '/images/map/square_shadow.png'
        },
        RW_group: {
            style:  'large_dot',
            image:  '/images/map/large_dot_blue.png',
            shadow: '/images/map/large_dot_shadow.png'
        },
        SC_plain: {
            style:  'smaller_dot',
            image:  '/images/map/smaller_dot_red.png',
            shadow: '/images/map/smaller_dot_shadow.png'
        },
        SC_brand: {
            style:  'square',
            image:  '/images/map/square_red.png',
            shadow: '/images/map/square_shadow.png'
        },
        SC_group: {
            style:  'large_dot',
            image:  '/images/map/large_dot_red.png',
            shadow: '/images/map/large_dot_shadow.png'
        },
        HS_plain: {
            style:  'smaller_dot',
            image:  '/images/map/smaller_dot_yellow.png',
            shadow: '/images/map/smaller_dot_shadow.png'
        },
        HS_brand: {
            style:  'square',
            image:  '/images/map/square_yellow.png',
            shadow: '/images/map/square_shadow.png'
        },
        HS_group: {
            style:  'large_dot',
            image:  '/images/map/large_dot_yellow.png',
            shadow: '/images/map/large_dot_shadow.png'
        },
        'default': {
            style:  'small_dot',
            image:  '/images/map/small_dot_yellow.png',
            shadow: '/images/map/small_dot_shadow.png'
        },
        mixed_group: {
            style:  'large_dot',
            image:  '/images/map/large_dot_grey.png',
            shadow: '/images/map/large_dot_shadow.png'
        },
        poi_food_store: {
            style:  'poi',
            image:  '/images/map/poi_food_store.png',
            shadow: '/images/map/poi_shadow.png'
        },
        poi_department: {
            style:  'poi',
            image:  '/images/map/poi_department.png',
            shadow: '/images/map/poi_shadow.png'
        },
        poi_petrol: {
            style:  'poi',
            image:  '/images/map/poi_petrol.png',
            shadow: '/images/map/poi_shadow.png'
        },
        poi_diy: {
            style:  'poi',
            image:  '/images/map/poi_diy.png',
            shadow: '/images/map/poi_shadow.png'
        },
        poi_RW: {
            style:  'poi_scheme',
            image:  '/images/map/poi_rw.png',
            shadow: '/images/map/poi_scheme_shadow.png'
        },
        poi_SC: {
            style:  'poi_scheme',
            image:  '/images/map/poi_sc.png',
            shadow: '/images/map/poi_scheme_shadow.png'
        }
    },
    views: {
        order: ['region','county','town','address','poi'],
        // clustering views
        region: {
            name:     'region',
            min_zoom: 1,
            max_zoom: 6,
            names:    { },
            clusters: { }
        },
        county: {
            name:     'county',
            min_zoom: 7,
            max_zoom: 7,
            names:    { },
            clusters: { }
        },
        town: {
            name:     'town',
            min_zoom:  8,
            max_zoom:  9,
            names:    { },
            clusters: { }
        },
        address: {
            name:     'address',
            min_zoom: 10,
            max_zoom: 20,
            names:    { },
            clusters: { }
        },
        // overlay view(s)
        poi: {
            min_zoom: 14,
            max_zoom: 20,
            name: 'poi',
            url: '/map/poi/activities',
            panel: 'poi_panel',
            markers: [ ],
            marker_ids: [ ]
        }
    }
};

function show_map(config) {
    if ( !GBrowserIsCompatible()) {
        this.log('Your browser does not support Google Maps');
//        alert('Your browser does not support Google Maps');
//        return;
    }
    return new CRMap(config);
}

function CRMap() {
    this.init.apply(this, arguments);
}

proto = CRMap.prototype;

proto.init = function(config) {
    config = config || { };

    // get the document element for the map
    var elemname  = config.element || DEFAULT.element;
    this.element  = document.getElementById(elemname);
    if (! this.element) {
        alert("The '" + elemname + "' element is not defined in the document");
        return;
    }

    var jqelem       = $(this.element);
    this.map         = new GMap2(this.element);
    this.width       = jqelem.width();
    this.height      = jqelem.height();
    this.icon_styles = config.icon_styles           || DEFAULT.icon_styles;
    this.icons       = config.icons                 || DEFAULT.icons;
    this.shadows     = config.shadows               || DEFAULT.shadows;
    this.info_width  = config.info_width            || DEFAULT.info_width;
    this.map_type    = config.map_type              || DEFAULT.map_type;
    this.overlay     = config.overlay               || DEFAULT.overlay;
    this.views       = config.views                 || DEFAULT.views;
    this.cluster     = config.cluster               || DEFAULT.cluster;
    this.group_icon  = config.group_icon            || DEFAULT.group_icon;
    this.group_name  = config.group_name            || DEFAULT.group_name;
    this.pause       = config.pause                 || DEFAULT.pause;
    this.zoom        = parseInt(config.zoom         || DEFAULT.zoom);
//    this.poi_zoom    = parseInt(config.poi_zoom     || DEFAULT.poi_zoom);
    this.latitude    = parseFloat(config.latitude   || DEFAULT.latitude);
    this.longitude   = parseFloat(config.longitude  || DEFAULT.longitude);
    this.sponsored   = config.sponsored             || 0;
    this.portfolio   = config.portfolio             || 0;
    this.poi_panel   = config.poi_panel;
    this.show_poi    = { 'poi_RW': 1, 'poi_SC': 1, 'poi_food_store': 1, 'poi_department': 1 };
    this.view        = '';
    this.markers     = [ ];
    this.ids         = { };
    this.addr_ids    = { };
    this.poi_markers = [ ];

    this.init_map(config);
};

proto.init_map = function(config) {

    if (config.controls) {
        this.map.addControl( new GLargeMapControl() );	
        this.map.addControl( new GMapTypeControl() );
        this.map.addControl( new GScaleControl() );
    }

    if (config.inset) {
        var width  = config.inset_width  || DEFAULT.inset_width;
        var height = config.inset_height || DEFAULT.inset_height;
        
        this.map.addControl(
            new GOverviewMapControl(),
            new GControlPosition(
                /* for some reason, BOTTOM_LEFT works on safari, even
                 * though it appears on the right in both safari and firefox!
                 */
                G_ANCHOR_BOTTOM_LEFT, 
                new GSize(width, height)
            )
        );
    }

    var mid_lat, mid_lng;
    var markers  = config.markers || [ ];
    var zoom     = parseInt(config.zoom || DEFAULT.zoom);

    if (config.latitude && config.longitude) {
        // caller has provided us with explicit lat/lng
        this.latitude    = parseFloat(config.latitude);
        this.longitude   = parseFloat(config.longitude);
    }
    else if (markers.length) {
        // if we haven't got a latitude and longitude then we compute 
        // them as the centroid of the markers
        this.log('Computing centroid of markers');
        var min_lat   =  1000;
        var min_lng   =  1000;
        var max_lat   = -1000;
        var max_lng   = -1000;
        var delta_lat = 0;
        var delta_lng = 0;
        var delta_max = 0;
        var addr_id;

        // compute the midpoint of lat and long
        for (n in markers) {
            marker = markers[n];
            if (marker[0] < min_lat) {
                min_lat = marker[0];
            }
            if (marker[0] > max_lat) {
                max_lat = marker[0];
            }
            if (marker[1] < min_lng) {
                min_lng = marker[1];
            }
            if (marker[1] > max_lng) {
                max_lng = marker[1];
            }
            
            // while we're here - mark the address id so we can prevent 
            // POIs from over laying an existing search result at the same
            // address
            addr_id = marker[2].address_id;
            if (addr_id) {
                this.log('marked address: ', addr_id);
                this.addr_ids[addr_id] = 1;
            }
        }
        
        // if we haven't got an explicit zoom config paramter then we 
        // guestimate it from the dispersion of the marker points
        if (! config.zoom) {
            delta_lat = max_lat - min_lat;
            delta_lng = max_lng - min_lng;
            delta_max = delta_lat > delta_lng ? delta_lat : delta_lng;
            this.log('delta max: %s', delta_max);
            // ad-hoc guestimate
            if (delta_max < 0.1)
                zoom = 12;
            else  if (delta_max < 0.2)
                zoom = 11;
            else if (delta_max < 0.31)
                zoom = 10;
            else if (delta_max < 0.75)
                zoom = 9;
            else if (delta_max < 1.25)
                zoom = 8;
            else if (delta_max < 2)
                zoom = 7;
            else if (delta_max < 4.7)
                zoom = 6;
            else 
                zoom = 5;
        }
        this.log("Latitude: %f - %f  (%f)", min_lat, max_lat, delta_lat);
        this.log("Longitude: %f - %f  (%f)", min_lng, max_lng, delta_lng);
        this.log("Centroid Zoom: %d", zoom);
        this.latitude  = (min_lat + max_lat)  / 2;
        this.longitude = (min_lng + max_lng) / 2;
    }
    else {
        this.latitude  = parseFloat(DEFAULT.latitude);
        this.longitude = parseFloat(DEFAULT.longitude);
    }

    // zoom may have been tweaked by marker dispersion, above
    this.zoom = zoom;

    this.log('Setting centre to %f,%f zoom:%d', this.latitude, this.longitude, zoom);

    this.map.setCenter(
        new GLatLng(this.latitude, this.longitude), 
        this.zoom, this.map_type
    );
    
    if (markers)
        this.add_markers(markers);
        
    if (size(this.overlay)) {
        this.log('binding zoom/move handlers for overlays');

        // bind the handlers
        GEvent.bind(this.map, "zoomend", this, this.changed);
        GEvent.bind(this.map, "moveend", this, this.changed);
    }

    // prime it
    this.changed();
};

proto.changed = function() {
    var old_zoom = this.zoom;
    var old_view = this.view;
    var new_zoom = this.zoom = this.map.getZoom();
    var new_view;
    
    this.log('changed.  old zoom: %s   new zoom: %s', old_zoom, new_zoom);

    // figure out what view mode we're in depending on zoom level
    for (var n in this.views.order) {
        var name = this.views.order[n];
        var view = this.views[name];

        // skip over any clustering views if we're not in clustering mode,
        // and any overlay views if we're not overlaying
        if ( (view.clusters && ! this.cluster)
          || (view.url      && ! size(this.overlay)) ) {
              this.log('skipping %s view (not enabled)', view.name);
              continue;
             }

        if (new_zoom >= view.min_zoom && new_zoom <= view.max_zoom) {
//          this.log('We are in %s view', view.name);
            // turn on the new view
            if (view.url) {
                // if the view has an autoload URL then we schedule it 
                // for some refreshment
                this.log('scheduling %s view for refreshment', view.name);
                this.load_view_soon(view);
                this.overlay_view = view;
            }
            else if (view.clusters) {
                if (old_view && old_view.name == view.name) {
//                  this.log('Still in %s view', view.name);
                }
                else {
                    // enable clustering view 
                    this.log('enabling %s view for clustering', view.name);
                    this.view = view;
                    this.refresh_view(view);
                }
            }

            // activate any control panel associated with this view
            if (view.panel) {
                var panel = this[view.panel];
                if (panel) {
//                  this.log('activating %s: %s', view.panel, panel);
                    $(panel).addClass('active');
                }
            }
        }
        else if (old_zoom != new_zoom && old_zoom >= view.min_zoom && old_zoom <= view.max_zoom) {
//          this.log('We used to be in %s view', view.name);
            // turn off the old view
            if (view.clusters) {
                this.log('disable previous cluster view');
                this.hide_view(view);
            }
            else if (view.url) {
                this.log('unloading previous autoloaded %s view', view.name);
//                this.hide_view(view);
                this.unload_view(view);
                delete this.overlay_view;
            }

            // de-activate any control panel associated with this view
            if (view.panel) {
                var panel = this[view.panel];
                if (panel) {
                    this.log('de-activating %s: %s', view.panel, panel);
                    $(panel).removeClass('active');
                }
            }

        }
    }
};

proto.refresh_view = function(view) {
    var markers = view.markers;
    var names   = view.names;

    if (! markers) {
        this.log('initialising view markers');
        markers = view.markers = [ ];

        var clusters = view.clusters;

        if (! names)
            names = view.names = { };

        // walk through each of the clusters in this view
        for (var i in clusters) {
            var cluster = clusters[i];
            var lat     = 0;
            var lng     = 0;
            var cats    = { };
            var cat;
            
            // walk through each of the points in the cluster, averaging
            // lng/lat and ...
//          this.log('cluster %s: $s', i, view.names[i]);
            for (var j in cluster) {
                var point = cluster[j];
//                this.log(' + %s,%s', point[0], point[1]);
                lat = lat + point[0];
                lng = lng + point[1];
                cat = point[2]['category'];
                if (cat) {
                    cats[cat] = cats[cat] || 0;
                    cats[cat]++;
                }
            }
            
            lat = lat / cluster.length;
            lng = lng / cluster.length;
//          this.log('cluster %s (%s) has %s items, positioned at %s, %s', i, view.names[i], cluster.length, lat, lng);

            if (size(cluster) == 1) {
                markers.push(
                    this.new_marker(lat, lng, cluster[0][2])
                );
            }
            else {
                var info     = { };
                var defaults = { 
                    label: cluster.length,
                    icon: size(cats) == 1 
                        ? keys(cats)[0] + '_group'
                        : this.group_icon
                };

//              this.log(
//                  "%s cluster contains %d categories: %s.  Icon: %s", 
//                  view.name, size(cats), keys(cats).join(', '), info.icon
//              );

                if (view.name == 'address') {
                    jQuery.extend(info, cluster[0][2], defaults);

                    if (info.scheme_id) {
//                      this.log('looks like a scheme: %s (%s)', info.scheme_id, info.name);
                        jQuery.extend(info, {
                            type: 'scheme',
                            link: '/scheme/' + info.scheme_id + '/to_let.html'
                        });
                        if (info.sponsored && (size(cats) == 1)) {
//                          this.log('looks like a sponsored scheme: %s (%s)', info.scheme_id, info.sponsored);
                            jQuery.extend(info, {
                                icon: keys(cats)[0] + '_brand',
                                label: ''
                            });
                        }
                    }
                }
                else {
                    jQuery.extend(info, defaults, {
                        compact: 1,
                        name:    view.names[i],
                        text:    size(cluster) + ' ' + this.group_name
                    });
                }
                    
                markers.push(
                    this.new_marker(lat, lng, info)
                );
            }
        }
    }

//  this.log('displaying %s cluster markers', markers.length);
    for (var m in markers) {
        this.map.addOverlay(markers[m]);
    }
        
};


/*------------------------------------------------------------------------
 * marker management
 *-----------------------------------------------------------------------*/
 
proto.add_markers = function(markers) {
    this.log('Adding %s markers', markers.length);

    for (var n in markers) {
        var marker = markers[n];
        var item   = marker[2];
        this.ids[item.id] = item;
//      this.log('Adding marker %s', item.id);
        this.add_marker(marker[0], marker[1], item);
    }
};

proto.add_marker = function(lat, lng, item) {
    /* check for invalid (0,0) co-ordinates - there were some in the legacy db */
    if (parseFloat(lat) == 0 && parseFloat(lng) == 0) {
//      this.log('ignoring marker at %s,%s (zero co-ordinates)', lat, lng);
        return;
    }

    /* skip duplicates */
//    if (item && item.id && this.ids[item.id]) {
//       this.log('skipping duplicate marker for %s', item.id);
//       return; 
//    }

    if (this.cluster) {
//      this.log('Clustering marker at %f,%f', lat, lng);
        this.cluster_marker(lat, lng, item);
    }
    else {
//      this.log('Adding marker at %f,%f', lat, lng);
        var marker = this.new_marker(lat, lng, item);
        this.map.addOverlay(marker);
        return marker;
    }
};

proto.cluster_marker = function(lat, lng, item) {
    // iterate through region, county and town views, using the region_id, 
    // county_id and town_id to cluster the items accordingly
    for (var n in this.views.order) {
        var name  = this.views.order[n];
        var view  = this.views[name];
        var idvar = name + '_id';
        var idval = item[idvar];
//      this.log('cluster %s => %s', idvar, idval);

        if (! idval) {
//          this.log('no id for %s', idvar);
            continue;
        }

        if (! view.names) 
            view.names = { };
        
        // keep track of the names of different clusters, e.g. Town 1234
        // is Guildford, County 4567 is Surrey, etc.
        if (view.names[idval]) {
//          this.log('already got %s name for %s: %s', name, idval, view.names[idval]);
        }
        else {
            view.names[idval] = item[name];
//          this.log('set %s name for %s: %s', name, idval, item[name]);
        }
        
        // initialise the cluster list first time we use it
        var cluster = view.clusters[idval];
        if (! cluster)
            cluster = view.clusters[idval] = [ ];
        
        // add the point data to the cluster
        cluster.push([lat,lng,item]);
    }
};

proto.new_marker = function(lat, lng, opts) {
    var name = opts.icon || 'default';
    var spec = this.icons[name] 
        || this.fail('Invalid icon name specified: ', name);
    var sname = spec.style 
        || this.fail('No style name for icon: ', name);
    var style = this.icon_styles[sname] || this.icon_styles['default']
        || this.fail('Unknown icon style: ', sname);
        
    var icon = new GIcon(G_DEFAULT_ICON);
    var size = new GSize(style.width, style.height);
    var marker;

/*
    this.log(
        "using icon style %s\nwith image: %s\nand shadow: %s", 
        name, spec.image, spec.shadow
    );
*/
    
    icon.image      = spec.image;
    icon.shadow     = spec.shadow;
    icon.iconSize   = size;
    icon.shadowSize = size;
    icon.imageMap   = style.map || [
        0,0,
        style.width,0,
        style.width,style.height,
        0,style.height
    ];
    icon.iconAnchor = new GPoint(
        style.anchor_x || 0, 
        style.anchor_y || style.height
    );

    icon.infoWindowAnchor = new GPoint(
        style.window_x || Math.floor(style.width  / 2), 
        style.window_y || Math.floor(style.height / 4)
    );
    
    if (opts.label) {
//      this.log('using labeled marker (sorry, the library uses US speling) at %s,%s', style.label_x, style.label_y);
        marker = new LabeledMarker(
            new GLatLng(lat, lng),  
            { 
                icon: icon,
                labelText: opts.label,
                labelOffset: new GSize(
                    style.label_x || 0, 
                    style.label_y || Math.floor(style.height / 2)
                )
            }
        );
    }
    else {
        marker = new GMarker( 
            new GLatLng(lat, lng),  
            { icon: icon, draggable: opts.on_drag ? true : false }
        );
        if (opts.on_drag) {
            var that = this;
            GEvent.addListener( marker, "dragend", 
                function() {
                    opts.on_drag(that, marker, opts);
                }
            );
        }
    }
    
    if (! opts.no_info) {
        var that = this;

        GEvent.addListener(marker, "click", function() {
            marker.openInfoWindowHtml(
                that.marker_info(opts), { 
                    maxWidth: that.info_width
//                  pixelOffset: new GSize(40,28),
                }
            );
//        if (info.callback) {
//            info.callback(that, lat, lng, info, marker);
//        }
        });
    }
        
    return marker;
};

proto.marker_info = function(info) {
    var text = '';
    var size = info.compact ? 10 : 25;
    var push = '';    // push title down if there's no picture
//  this.log('%s is compact? %s => %s', info.name, info.compact, size);
    
    if (this.sponsored && (info.picture || info.logo)) {
        size = size + (this.portfolio ? 150 : 40);
        push = ' push';
        if (info.picture) {
            text = text + this.elem('img', {'src': info.picture, 'class':'picture'});
        }
        if (info.logo) {
            text = text + this.elem('img', {'src': info.logo, 'class':'logo'});
        }
    }
    
    text = text + this.elem('h4',  {'class':'name' + push}, info.name);

    if (info.address) {
        text = text + this.elem('div', { 'class': 'address' }, info.address);
        size += 45;
    }

    if (info.text) {
        text = text + this.elem('div', { 'class': 'address' }, info.text);
        size += 20;
    }

    if (info.catname) {
        text = text + this.elem(
            'div', { 'class': 'category' }, 
            this.elem( 'span', { 'class': info.category }, info.catname )
        );
        size += 25;
    }
    
    if (info.link) {
        text = text + this.elem('a',  {'href':info.link, 'class':'link'}, info.link_text || ('View ' + (info.type ? info.type : 'Scheme')));
        size += 25;
    }

    // hacks for address debugging
    if (info.latitude && this.debug) {
        text = text + this.elem('div',  {'class':'lat'}, 'Lat: ' + info.latitude + '<br>Lng: ' + info.longitude);
  //      size += 40;
    }

    text = text + this.elem('div',  {'class':'line'}, '');
    
//  this.log('text: %s', text);
//  this.log('total size: %s', size);


    return this.elem('div', {'class':'popup', 'style': 'height: ' + size + 'px'}, text);
};


/*------------------------------------------------------------------------
 * dynamic marker loading
 *-----------------------------------------------------------------------*/

proto.load_view_soon = function(view) {
    if (this.view_soon) {
        /* we may have a new view to replace the old one */
        this.view_soon = view;
//      this.log('already waiting to update view');
    }
    else {
        var self = this;
        /* store the view we want to load and set a timer */
        this.view_soon = view;
        setTimeout(
            function () { 
                /* make sure we use the value in this.view_soon which may
                 * have been updated since we set it */
                var v = self.view_soon;
                delete self.view_soon;
                self.load_view(v);
            },
            this.pause
        );
//      this.log('waiting to load view...');
    }
};

proto.load_view = function(view) {
    var bounds  = this.map.getBounds();
    var ne      = bounds.getNorthEast();
    var sw      = bounds.getSouthWest();
    var north   = ne.lat();
    var east    = ne.lng();
    var south   = sw.lat();
    var west    = sw.lng();
    var self    = this;
    var overlay = this.overlay;
    var url     = view.url;
    if (! url) {
//      this.log('no url to load view %s', view.name);
        return;
    }
//    this.log('Loading listings via AJAX: %s', url);
    $.getJSON(
        url,
        { n:    north,
          s:    south,
          e:    east,
          w:    west,
          RW:   overlay.RW  || 0,
          SC:   overlay.SC  || 0,
          POI:  overlay.POI || 0,
          portfolio_id: this.portfolio,
          json: 1
        },
        function(data) { self.loaded_view(view, data); }
    );
};

proto.hide_view = function(view) {
    var markers  = view.markers;
    
    this.log('hiding %s markers', view.name);
    for (var n in markers) {
        var item = markers[n];
        if (item) {
            this.map.removeOverlay(item);
        }
    }
};  

proto.unload_view = function(view) {
    var markers    = view.markers;
    var marker_ids = view.marker_ids;
    
    this.hide_view(view);
    this.log('unloading %s markers', view.name);
    
    for (var n in marker_ids) {
//      this.log('removing marker: %s', marker_ids[n]);
        delete this.ids[ marker_ids[n] ];
    }
};  

proto.loaded_view = function(view, data) {
    this.log('Loaded %s view with %s items', view.name, data.length);

    for (var n in data) {
        var item    = data[n];
        var lat     = parseFloat(item.latitude);
        var lng     = parseFloat(item.longitude);
        var addr_id = item.address_id;

        // must have a lat/lng - TODO: NaN?
        if (lat == 0 || lng == 0) {
            this.log("skipping item (no lat/lng)");
            continue;
        }

        // must have a record ID
        if (! item.id) {
            this.log("skipping item (no id)");
            continue;
        }
        
        if (item.category && ! this.overlay[item.category]) {
            this.log("skipping item (not overlaying %s)", item.category);
            continue;
        }
        
        if (item.id in this.ids) {
            this.log('skipping existing entry for %s', item.id);
        }
        else if (view.url && this.addr_ids[addr_id]) {
            this.log("skipping POI at marker address: %s", addr_id);
        }
        else if (! this.show_poi[item.icon]) {
            this.log("skipping inactive POI: %s", item.icon);
        }
        else {
            this.ids[item.id] = item;
            view.marker_ids.push(item.id);
            this.log('adding marker for %s', item.id);
            var marker = this.new_marker(lat, lng, item);
            this.map.addOverlay(marker);
            item.marker = marker;
            view.markers.push(marker);
        }
    }
};

proto.toggle_poi = function(code,value) {
    this.log('toggle %s to %s', code, value);
    
    // don't both if the item is already on/off
    if ((value && this.show_poi[code])
    || ((! value) && (! this.show_poi[code])))
        return;

    this.show_poi[code] = value;
    
    if (value) {
        // calling changed() method will trigger reload
        this.changed();
    }
    else {
        var view = this.overlay_view;
//      this.log('view: %s  url: %s', view.name, view.url);
        if (view && view.url) {
            var mids = view.marker_ids;
            for (var m in mids) {
                var mid  = mids[m];
                var item = this.ids[mid];
//              this.log('checking category of %s: %s vs %s', mid, item.icon, code);
                if (item && item.icon == code) {
                    this.map.removeOverlay(item.marker);
                    delete mids[m];
                    delete this.ids[mid];
                }
            }
        }
    }
        
//    this.refresh_view(this.view);
};

/*------------------------------------------------------------------------
 * miscellaneous helper methods/functions
 *-----------------------------------------------------------------------*/

proto.elem = function(etype, attrs, ebody) {
    return '<' + etype + this.elem_attrs(attrs) + (ebody
         ? '>' + ebody + '</' + etype + '>'
         : '/>');
};

proto.elem_attrs = function(attrs) {
    var name, value;
    var chunks = [ ];
    for (name in attrs) {
        value = attrs[name];
        chunks.push(name + '="' + value + '"');
    }
    return chunks.length
        ? ' ' + chunks.join(' ')
        : '';
};

proto.fail = function() {
    var out = '';
    for(var i = 0; i < arguments.length; i++) {
        out += (arguments[i] == undefined) ? "" : arguments[i].toString();
    }
    throw out;
};

proto.log = function() {
    if (window.console && window.console.log)
        window.console.log.apply(console, arguments);
};

function keys(obj) {
    var keys = [];
    for(var key in obj){
        keys.push(key);
    }
    return keys;
}

function size(obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};


