// vim: et:ts=4:sw=4:sts=4:ft=js

/*@
Ox.Map <function> Basic map object
    # DESCRIPTION --------------------------------------------------------------
    <code>Ox.Map</code> is a wrapper around the
    <a href="http://code.google.com/apis/maps/documentation/javascript/">Google
    Maps API</a>.
    # USAGE --------------------------------------------------------------------
    () ->              <f> Map object
    (options) ->       <f> Map object
    (options, self) -> <f> Map object
    # ARGUMENTS ----------------------------------------------------------------
    options <o|{}> options
        clickable       <b|false> If true, clicking on the map finds a place
        editable        <b|false> If true, places are editable
        findPlaceholder <s|"Find"> Placeholder text for the find input element
        labels          <b|false> If true, show labels on the map
        markers         <n|100> Maximum number of markers to be displayed
        places          <[o]|[]> Array of place objects
            countryCode     <s> ISO 3166 country code
            east            <n> Longitude of the eastern boundary in degrees
            editable        <b|false> If true, the place is editable
            geoname         <s> Geoname (like "Paris, Île-de-France, France")
            lat             <n> Latitude in degrees
            lng             <n> Longitude in degrees
            markerColor     <s|"red"> CSS color of the place marker
            markerSize      <n|16> size of the place marker in px
            name            <s> Name (like "Paris")
            north           <n> Latitude of the northern boundary in degrees
            south           <n> Latitude of the southern boundary in degrees
            type            <s> Type (like "city" or "country")
            west            <n> Longitude of the western boundary in degrees
        selected        <s|""> Id of the selected place
        statusbar       <b|false> If true, the map has a statusbar
        toolbar         <b|false> If true, the map has a toolbar
    self <o|{}> Shared private variable
    # EVENTS -------------------------------------------------------------------
    addplace <!> Fires when a place has been added
        place <o> Place object
    editplace <!> Fires when a place has been edited
        place <o> Place object
    geocode <!> Fires when a google geocode request returns
        latLng <o|u> Query coordinates, or undefined
            lat <n> latitude
            lng <n> longitude
        address <s|u> Query string, or undefined
        results <[o]> Google Maps geocode results
            address_components <[o]> Address components
                long_name <s> Long name
                short_name <s> Short name
                types <[s]> Types (like "country" or "political")
            formatted_address <s> Formatted address
            geometry <o> Geometry
                bounds <o> Bounds
                    northEast <o> North-east corner
                        lat <n> Latitude
                        lng <n> Longitude
                    southWest <o> South-west corner
                        lat <n> Latitude
                        lng <n> Longitude
                location <o> Location
                    lat <n> Latitude
                    lng <n> Longitude
                location_type <s> Location type (like "APPROXIMATE")
                viewport <o> Viewport
                    northEast <o> North-east corner
                        lat <n> Latitude
                        lng <n> Longitude
                    southWest <o> South-west corner
                        lat <n> Latitude
                        lng <n> Longitude
            types <[s]> Types (like "country" or "political")
    # EXAMPLES -----------------------------------------------------------------
    <script>
        // simple
        Ox.load('UI', function() {
            Ox.Map().appendTo(Ox.UI.$body);
        });
    </script>
    > Ox.Map() === true
    false
    > Ox.Map() === false
    false
@*/

Ox.Map = function(options, self) {

    self = self || {};
    var that = new Ox.Element({}, self)
        .defaults({
            // fixme: isClickable? hasZoombar?
            clickable: false,
			editable: false,
			findPlaceholder: 'Find',
            labels: false,
            markers: 100,
            places: [],
            selected: null,
            statusbar: false,
            toolbar: false,
            zoombar: false
        })
        .options(options || {})
        .addClass('OxMap')
        .click(function(e) {
            !$(e.target).is('input') && that.gainFocus();
        })
		.bindEvent({
			key_0: function() {
			    that.panToPlace()
			},
            key_down: function() {
				pan(0, 1);
			},
            key_enter: pressEnter,
            key_escape: pressEscape,
			key_equal: function() {
				zoom(1);
			},
			key_l: toggleLabels,
            key_left: function() {
				pan(-1, 0);
			},
			key_meta: function() {
			    self.metaKey = true;
			    $(document).one({
			        keyup: function() {
			            self.metaKey = false;
			        }
			    });
			},
			key_minus: function() {
				zoom(-1);
			},
            key_right: function() {
				pan(1, 0);
			},
			key_shift: function() {
			    self.shiftKey = true;
			    $(document).one({
			        keyup: function() {
			            self.shiftKey = false;
			        }
			    });
			},
			key_shift_down: function() {
			    pan(0, 2);
			},
			key_shift_0: function() {
			    that.zoomToPlace();
			},
			key_shift_equal: function() {
			    zoom(2)
			},
			key_shift_left: function() {
			    pan(-2, 0);
			},
			key_shift_minus: function() {
			    zoom(-2);
			},
			key_shift_right: function() {
			    pan(2, 0);
			},
			key_shift_up: function() {
			    pan(0, -2);
			},
            key_up: function() {
				pan(0, -1);
			},
			key_z: undo
        });

    self.mapHeight = getMapHeight();
    self.minZoom = getMinZoom();
    self.scaleMeters = [
        50000000, 20000000, 10000000,
        5000000, 2000000, 1000000,
        500000, 200000, 100000,
        50000, 20000, 10000,
        5000, 2000, 1000,
        500, 200, 100,
        50, 20, 10
    ];

    Ox.extend(self, {
        metaKey: false,
        resultPlace: null,
        shiftKey: false
    });

    if (self.options.toolbar) {
        self.$toolbar = new Ox.Bar({
                size: 24
            })
            .appendTo(that);
        self.$labelsButton = new Ox.Button({
                title: 'Show Labels',
                width: 96
            })
            .css({float: 'left', margin: '4px'})
            .bindEvent({
                click: toggleLabels
            })
            .appendTo(self.$toolbar)
        self.$findInput = new Ox.Input({
                clear: true,
                placeholder: self.options.findPlaceholder,
                width: 192
            })
            .css({float: 'right', margin: '4px'})
            .bindEvent({
                submit: submitFind
            })
            .appendTo(self.$toolbar)
    }

    self.$map = new Ox.Element()
        .css({
            left: 0,
            top: self.options.toolbar * 24 + 'px',
            right: 0,
            bottom: self.options.zoombar * 16 + self.options.statusbar * 24 + 'px'
        })
        .appendTo(that);

    if (self.options.zoombar) {
        self.$zoombar = new Ox.Bar({
                size: 16
            })
            .css({
                bottom: self.options.statusbar * 24 + 'px'
            })
            .appendTo(that);
    }

    if (self.options.statusbar) {
        self.$statusbar = new Ox.Bar({
                size: 24
            })
            .css({
                bottom: 0
            })
            .appendTo(that);
        self.$placeNameInput = new Ox.Input({
                placeholder: 'Name',
                width: 96
            })
            //.css({position: 'absolute', left: 4, top: 4})
            .css({float: 'left', margin: '4px 1px 4px 4px'})
            .appendTo(self.$statusbar);
        self.$placeGeonameInput = new Ox.Input({
                placeholder: 'Geoname',
                width: 96
            })
            //.css({position: 'absolute', left: 104, top: 4})
            .css({float: 'left', margin: '4px 1px 4px 4px'})
            .appendTo(self.$statusbar);
        self.$placeButton = new Ox.Button({
                title: 'New Place',
                width: 96
            })
            .css({float: 'right', margin: '4px 4px 4px 2px'})
            .bindEvent({
                click: clickPlaceButton
            })
            .appendTo(self.$statusbar);
    }

    self.$navigationButtons = {
        'center': new Ox.Button({
                title: 'close',
                type: 'image'
            })
            .addClass('OxMapButton')
            .css({
                left: '24px',
                top: '24px'
            }),
        'east': new Ox.Button({
                title: 'right',
                type: 'image'
            })
            .addClass('OxMapButton')
            .css({
                left: '44px',
                top: '24px',
            }),
        'north': new Ox.Button({
                title: 'up',
                type: 'image'
            })
            .addClass('OxMapButton')
            .css({
                left: '24px',
                top: '4px',
            }),
        'south': new Ox.Button({
                title: 'down',
                type: 'image'
            })
            .addClass('OxMapButton')
            .css({
                left: '24px',
                top: '44px',
            }),
        'west': new Ox.Button({
                title: 'left',
                type: 'image'
            })
            .addClass('OxMapButton')
            .css({
                left: '4px',
                top: '24px',
            })
    };

    self.$scaleLabel = new Ox.Label({
            textAlign: 'center',
            title: '...'
        })
        .addClass('OxMapLabel')
        .css({
            right: '4px',
            top: '4px'
        });

    if (!window.googleCallback) {
        window.googleCallback = function() {
            delete window.googleCallback;
            initMap();
        };
        $.getScript('http://maps.google.com/maps/api/js?callback=googleCallback&sensor=false');
    } else {
        (function interval() {
            window.google ? initMap() : setTimeout(interval, 100);
        }());
    }

    function addPlaceToMap(place) {
        // via find, click, or new place button
        var exists = false;
        if (!place) {
            var bounds = self.map.getBounds(),
                center = self.map.getCenter(),
                southwest = new google.maps.LatLngBounds(
                    bounds.getSouthWest(), center
                ).getCenter(),
                northeast = new google.maps.LatLngBounds(
                    center, bounds.getNorthEast()
                ).getCenter(),
                place = new Ox.MapPlace({
                    countryCode: '',
                    editable: true,
                    geoname: '',
                    id: '_' + Ox.uid(), // fixme: stupid
                    lat: center.lat(),
                    lng: center.lng(),
                    map: that,
                    name: '',
                    south: southwest.lat(),
    		        west: southwest.lng(),
    		        north: northeast.lat(),
    		        east: northeast.lng()
                });
        }
        Ox.forEach(self.places, function(p, i) {
            if (place.bounds.equals(p.bounds)) {
                place = p;
                exists = true;
                return false;
            }
        });
        if (!exists) {
            self.resultPlace && self.resultPlace.remove();
            self.resultPlace = place;
            place.add();
        }
        selectPlace(place.id);
    }

    function addPlaceToPlaces() {
        var place = getSelectedPlace();
        if (self.options.selected == place.id) {
            self.options.selected = place.id.substr(1);
        }
        place.id = place.id.substr(1); // fixme: NOT SAFE!
        place.name = self.$placeNameInput.value();
        place.geoname = self.$placeGeonameInput.value();
        place.countryCode = Ox.getCountryCode(place.geoname);
        place.marker.update();
        self.places.push(place);
        self.resultPlace = null;
        that.triggerEvent('addplace', place)
        Ox.print('SSSS', self.options.selected)
    }

    function boundsChanged() {
        setScale();
        self.boundsChanged = true;
    }

    function canContain(outerBounds, innerBounds) {
        // checks if outerBounds _can_ contain innerBounds
        var outerSpan = outerBounds.toSpan(),
            innerSpan = innerBounds.toSpan();
        return outerSpan.lat() > innerSpan.lat() &&
            outerSpan.lng() > innerSpan.lng();
    }

    function centerChanged() {
        self.center = self.map.getCenter();
        self.centerChanged = true;
    }

    function changeZoom(event, data) {
        self.map.setZoom(data.value);
    }

    function clickMap(event) {
        Ox.print('Ox.Map clickMap')
		if (self.options.clickable/* && !editing()*/) {
	        getPlaceByLatLng(event.latLng, self.map.getBounds(), function(place) {
	            Ox.print('>>>>', place)
                if (place) {
                    addPlaceToMap(place);
                    //selectPlace(place.id);
                } else {
                    selectPlace(null);
                }
            });
		}
    }

    function clickPlaceButton() {
        var place = getSelectedPlace(),
            title = self.$placeButton.options('title');
        if (title == 'New Place') {
            addPlaceToMap();
        } else if (title == 'Add Place') {
            addPlaceToPlaces();
        } else if (title == 'Remove Place') {
            
        }
    }

    function constructZoomInput() {
        Ox.print('constructZoomInput', self.minZoom, self.maxZoom)
        if (self.options.zoombar) {
            self.$zoomInput && self.$zoomInput.removeElement();
            self.$zoomInput = new Ox.Range({
                    arrows: true,
                    max: self.maxZoom,
                    min: self.minZoom,
                    size: that.width(),
                    thumbSize: 32,
                    thumbValue: true,
                    value: self.map.getZoom()
                })
                .bindEvent({
                    change: changeZoom
                })
                .appendTo(self.$zoombar);
        }
    }

    function editing() {
        var place = getSelectedPlace();
        return place && place.editing;
    }

    function getElevation(point, callback) {
        // fixme: unused
        if (arguments.length == 1) {
            callback = point;
            point = self.map.getCenter();
        }
        self.elevationService.getElevationForLocations({
            locations: [point]
        }, function(data) {
            callback(data.elevation);
        });
    }

    function getMapHeight() {
        return self.options.height -
            self.options.statusbar * 24 -
            self.options.toolbar * 24 -
            self.options.zoombar * 16;
    }

    function getMapType() {
        return self.options.labels ? 'HYBRID' : 'SATELLITE'
    }

    function getMaxZoom(point, callback) {
        if (arguments.length == 1) {
            callback = point;
            point = self.map.getCenter();
        }
        self.maxZoomService.getMaxZoomAtLatLng(point, function(data) {
            callback(data.status == 'OK' ? data.zoom : null);
        });
    }

    function getMinZoom() {
        return 0;
        return Math.ceil(
            Ox.log(self.mapHeight / Ox.MAP_TILE_SIZE, 2)
        );
    }

    function getPlaceById(id) {
        var place = Ox.getObjectById(self.places, id);
        if (!place && self.resultPlace && self.resultPlace.id == id) {
            place = self.resultPlace;
        }
        Ox.print('getPlaceById', id, place)
        return place;
    }

    function getPlaceByLatLng(latlng, bounds, callback) {
        // gets the largest place at latlng that would fit in bounds
        Ox.print('ll b', latlng, bounds)
        var callback = arguments.length == 3 ? callback : bounds,
            bounds = arguments.length == 3 ? bounds : null;
        self.geocoder.geocode({
            latLng: latlng
        }, function(results, status) {
            Ox.print('results', results)
            var length = results.length;
            if (status == google.maps.GeocoderStatus.OK) {
                if (bounds) {
                    Ox.forEach(results.reverse(), function(result, i) {
                        if (
                            i == length - 1 ||
                            canContain(bounds, result.geometry.bounds || result.geometry.viewport)
                        ) {
                            callback(new Ox.MapPlace(parseGeodata(results[i])));
                            return false;
                        }
                    });
                } else {
                    callback(new Ox.MapPlace(parseGeodata(results[0])));
                }
            }
            if (
                status == google.maps.GeocoderStatus.OK ||
                status == google.maps.GeocoderStatus.ZERO_RESULTS
            ) {
                triggerGeocodeEvent({
                    latLng: latlng,
                    results: results
                });
            } else {
                Ox.print('geocode failed:', status);
                callback(null);
            }
        });
    }

    function getPlaceByName(name, callback) {
        self.geocoder.geocode({
            address: name
        }, function(results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                callback(new Ox.MapPlace(parseGeodata(results[0])));
            }
            if (
                status == google.maps.GeocoderStatus.OK ||
                status == google.maps.GeocoderStatus.ZERO_RESULTS
            ) {
                triggerGeocodeEvent({
                    address: name,
                    results: results
                });
            } else {
                Ox.print('geocode failed:', status);
                callback(null);
            }
        });
    }

	function getPositionByName(name) {
		var position = -1;
		Ox.forEach(self.options.places, function(place, i) {
			if (place.name == name) {
				position = i;
				return false;
			}
		});
		return position;
	}

    function getSelectedMarker() {
        // needed in case self.options.selected
        // is changed from outside
        var id = null;
        if (self.resultPlace && self.resultPlace.selected) {
            id = self.resultPlace.id;
        } else {
            Ox.forEach(self.places, function(place) {
                if (place.selected) {
                    id = place.id;
                    return false;
                }
            });
        }
        return id;
    }

    function getSelectedPlace() {
        return self.options.selected ?
            getPlaceById(self.options.selected) : null;
    }

    function initMap() {
        var mapBounds;

        updateFormElements();

        self.elevationService = new google.maps.ElevationService();
        self.geocoder = new google.maps.Geocoder();
        self.maxZoomService = new google.maps.MaxZoomService();
        self.places = [];
        self.options.places.forEach(function(place, i) {
            var bounds = new google.maps.LatLngBounds(
                    new google.maps.LatLng(place.south, place.west),
                    new google.maps.LatLng(place.north, place.east)
                );
            if (Ox.isUndefined(place.id)) {
                place.id = Ox.uid();
            }
            mapBounds = i == 0 ? bounds : mapBounds.union(bounds);
        });
        self.center = mapBounds ? mapBounds.getCenter() : new google.maps.LatLng(0, 0);
        self.zoom = 1; // fixme: should depend on height
        that.map = self.map = new google.maps.Map(self.$map.$element[0], {
            center: self.center,
            disableDefaultUI: true,
            disableDoubleClickZoom: true,
            mapTypeId: google.maps.MapTypeId[getMapType()],
            zoom: self.zoom
        });
        google.maps.event.addListener(self.map, 'bounds_changed', boundsChanged);
        google.maps.event.addListener(self.map, 'center_changed', centerChanged);
        google.maps.event.addListener(self.map, 'click', clickMap);
        google.maps.event.addListener(self.map, 'idle', mapChanged);
        google.maps.event.addListener(self.map, 'zoom_changed', zoomChanged);
        google.maps.event.addListenerOnce(self.map, 'tilesloaded', tilesLoaded);
        mapBounds && self.map.fitBounds(mapBounds);
        /*
        setTimeout(function() {
        }, 1000);
        */
        self.options.places.forEach(function(place, i) {
            self.places[i] = new Ox.MapPlace(Ox.extend({
                map: that
            }, place))/*.add()*/;
        });
        google.maps.event.trigger(self.map, 'resize');
		//that.gainFocus();
		that.triggerEvent('load');
		function tilesLoaded() {
		    // fixme: can add earlier, use don't replace map contents option
            Ox.forEach(self.$navigationButtons, function(button) {
                button.appendTo(self.$map);
            });
            self.$scaleLabel.appendTo(self.$map);
		}
    }

    function mapChanged() {
        // gets called after panning or zooming
        Ox.print('mapChanged');
        var bounds;
        if (self.boundsChanged) {
            bounds = self.map.getBounds();
            self.places.sort(function(a, b) {
                var sort = {
                    a: a.selected ? Infinity : 
                        bounds.contains(a.center) ? a.size : -Infinity,
                    b: b.selected ? Infinity : 
                        bounds.contains(b.center) ? b.size : -Infinity,
                };
                return sort.b - sort.a;
            }).forEach(function(place, i) {
                if (i < self.options.markers && !place.visible) {
                    place.add();
                } else if (i >= self.options.markers && place.visible) {
                    place.remove();
                }
            });
            self.boundsChanged = false;
        }
        if (self.centerChanged) {
            getMaxZoom(function(zoom) {
                if (zoom != self.maxZoom) {
                    self.maxZoom = zoom;
                    if (self.map.getZoom() > zoom) {
                        self.map.setZoom(zoom);
                    }
                    constructZoomInput();
                }
            });
            self.centerChanged = false;
        }
        if (self.zoomChanged) {
            self.zoomChanged = false;
        }
    }

	function pan(x, y) {
		self.map.panBy(x * self.$map.width() / 2, y * self.$map.height() / 2);
	};

    function parseGeodata(data) {
        var bounds = data.geometry.bounds || data.geometry.viewport,
            place = {
                components: data.address_components,
		        countryCode: getCountryCode(data.address_components),
		        east: bounds.getNorthEast().lng(),
		        editable: self.options.editable,
		        fullGeoname: getFullGeoname(data.address_components),
		        geoname: data.formatted_address,
		        id: '_' + Ox.uid(),
		        map: that,
                name: data.formatted_address.split(', ')[0],
		        north: bounds.getNorthEast().lat(),
		        south: bounds.getSouthWest().lat(),
		        types: data.types.map(function(type) {
		            return Ox.toTitleCase(type.replace(/_/g, ' '));
		        }),
		        west: bounds.getSouthWest().lng()
            };
        function getCountryCode(components) {
            countryCode = '';
            Ox.forEach(components, function(component) {
                if (component.types.indexOf('country') > -1) {
                    countryCode = component.short_name;
                    return false;
                }
            });
            return countryCode;
        }
        function getFullGeoname(components) {
            var country = false;
            return components.map(function(component, i) {
                var name = component.long_name;
                if (i && components[i - 1].types.indexOf('country') > -1) {
                    country = true;
                }
                return !country && (
                    i == 0 || name != components[i - 1].long_name
                ) ? name : null;
	        }).join(', ')
        }
	    return place;
    }

    function pressEnter() {
        var place = getSelectedPlace();
        if (place) {
            if (place.editing) {
                place.submit();
            } else {
                place.edit();
            }
        } else if (self.resultPlace) {
            selectPlace(self.resultPlace.id)
        }
    }

    function pressEscape() {
        var place = getSelectedPlace();
        if (place) {
            if (place.editing) {
                place.cancel();
            } else {
                selectPlace(null);
            }
        } else if (self.resultPlace) {
            self.resultPlace.remove();
            self.resultPlace = null;
        }
    }

    function removePlace(id) {
        
    }

	function reset() {
		//Ox.print(self.map.getZoom(), self.zoom);
		self.map.getZoom() == self.zoom ?
			self.map.panTo(self.center) :
			self.map.fitBounds(self.bounds);
	}

    function resizeMap() {
        /*
        Ox.print('resizeMap', self.options.width, self.options.height);
        var center = self.map.getCenter();
        self.mapHeight = getMapHeight();
        self.minZoom = getMinZoom();
        that.css({
            height: self.options.height + 'px',
            width: self.options.width + 'px'
        });
        self.$map.css({
            height: self.mapHeight + 'px',
            width: self.options.width + 'px'
        });
        google.maps.event.trigger(self.map, 'resize');
        self.map.setCenter(center);
        */
    }

    function selectPlace(id) {
        var place,
            selected = getSelectedMarker();
        Ox.print('Ox.Map selectPlace()', id, selected);
        if (id != selected) {
            place = getPlaceById(selected);
            place && place.deselect();
            place = getPlaceById(id);
            place && place.select();
            self.options.selected = id;
            setStatus();
            that.triggerEvent('selectplace', place);
        }
    };

    function getMetersPerPixel() {
        var mapWidth = self.$map.width(),
            span = self.map.getBounds().toSpan().lng();
        if (span >= 360) {
            span = 360 * mapWidth / Ox.MAP_TILE_SIZE;
        }
        return span * Ox.getMetersPerDegree(self.map.getCenter().lat()) / mapWidth;
    }

    function setScale() {
        var metersPerPixel = getMetersPerPixel();
        Ox.forEach(self.scaleMeters, function(meters) {
            var scaleWidth = Math.round(meters / metersPerPixel);
            if (scaleWidth <= 256) {
                self.$scaleLabel
                    .options({
                        title: '\u2190 ' + (
                            meters > 1000 ? Ox.formatNumber(meters / 1000) + ' k' : meters + ' '
                        ) + 'm \u2192'
                    })
                    .css({
                        width: (scaleWidth - 10) + 'px'
                    })
                return false;
            }
        });
    }

    function setStatus() {
        Ox.print('setStatus()', self.options.selected)
        var disabled, place, title;
        if (self.options.statusbar) {
            place = getSelectedPlace();
            if (place) {
                title = place.id[0] == '_' ? 'Add Place' : 'Remove Place';
            } else {
                title = 'New Place';
            }
            disabled = place && !place.editable;
            self.$placeNameInput.options({
                disabled: disabled,
                value: place ? place.name : ''
            });
            self.$placeGeonameInput.options({
                disabled: disabled,
                value: place ? place.geoname : ''
            });
            self.$placeButton.options({
                disabled: disabled,
                title: title
            });
        }
        Ox.print('STATUS DONE');
    }

    function submitFind(event, data) {
        that.findPlace(data.value, function(place) {
            setStatus(place);
        });
    }

    function toggleLabels() {
        self.options.labels = !self.options.labels
        self.map.setMapTypeId(google.maps.MapTypeId[getMapType()]);
        self.$labelsButton.options({
            title: self.$labelsButton.options('title') == 'Show Labels' ?
                'Hide Labels' : 'Show Labels'
        });
    }

    function triggerGeocodeEvent(data) {
        // someone may want to cache google geocode data, so we fire an event.
        // google puts functions like lat or lng on the objects' prototypes,
        // so we create properly named properties, for json encoding
        if (data.latLng) {
            data.latLng = {
                lat: data.latLng.lat(),
                lng: data.latLng.lng()
            }
        }
        data.results.forEach(function(result) {
            ['bounds', 'viewport'].forEach(function(key) {
                if (result.geometry[key]) {
                    result.geometry[key] = {
                        northEast: {
                            lat: result.geometry[key].getNorthEast().lat(),
                            lng: result.geometry[key].getNorthEast().lng()
                        },
                        southWest: {
                            lat: result.geometry[key].getSouthWest().lat(),
                            lng: result.geometry[key].getSouthWest().lng()
                        }
                    }
                }
            });
            if (result.geometry.location) {
                result.geometry.location = {
                    lat: result.geometry.location.lat(),
                    lng: result.geometry.location.lng()
                }
            }
        });
        that.triggerEvent('geocode', data);
    }

    function undo() {
        Ox.print('Map undo')
        var place = getSelectedPlace();
        place.editing && place.undo();
    }

    function updateFormElements() {
        var width = that.width();
        self.$zoomInput && constructZoomInput();
        self.$placeNameInput && self.$placeNameInput.options({
            width: Math.floor((width - 112) / 2)
        });
        self.$placeGeonameInput && self.$placeGeonameInput.options({
            width: Math.ceil((width - 112) / 2)
        });
    }

	function zoom(z) {
		self.map.setZoom(self.map.getZoom() + z);
	}

    function zoomChanged() {
        var zoom = self.map.getZoom();
        if (zoom < self.minZoom) {
            self.map.setZoom(self.minZoom);
        } else if (self.maxZoom && zoom > self.maxZoom) {
            self.map.setZoom(self.maxZoom);
        } else {
            self.zoomChanged = true;
            self.$zoomInput && self.$zoomInput.options({value: zoom});
            that.triggerEvent('zoom', {
                value: zoom
            });
        }
    }

	function zoomToPlace() {
	    Ox.print('zoomToPlace')
		if (self.options.selected !== null) {
			self.map.fitBounds(getPlaceById(self.options.selected).bounds);
		}
	}

    self.setOption = function(key, value) {
        /*if (key == 'height' || key == 'width') {
            resizeMap();
        } else */if (key == 'places') {
            loadPlaces();
        } else if (key == 'selected') {
            selectPlace(value);
        } else if (key == 'type') {
            
        }
    };

    that.getKey = function() {
        var key = null;
        if (self.shiftKey) {
            key = 'shift'
        } else if (self.metaKey) {
            key = 'meta'
        }
        return key;
    }

    that.editPlace = function() {
        getPlaceById(self.options.selected).edit();
		return that;
    }

    that.findPlace = function(name, callback) {
        getPlaceByName(name, function(place) {
            if (place) {
                addPlaceToMap(place);
                self.map.fitBounds(place.bounds);
            } else {
                self.$findInput.addClass('OxError');
            }
            callback(place);
        });
    };

	that.panToPlace = function() {
	    Ox.print('panToPlace:', self.options.selected)
	    var place = getSelectedPlace();
		place && self.map.panTo(place.center);
		return that;
	};

    that.removePlace = function(id) {
		return that;            
    };

    that.resizeMap = function() {

        /*
        Ox.print('resizeMap', self.options.width, self.options.height);
        var center = self.map.getCenter();
        self.mapHeight = getMapHeight();
        self.minZoom = getMinZoom();
        that.css({
            height: self.options.height + 'px',
            width: self.options.width + 'px'
        });
        self.$map.css({
            height: self.mapHeight + 'px',
            width: self.options.width + 'px'
        });
        google.maps.event.trigger(self.map, 'resize');
        self.map.setCenter(center);
        */

        /*
        Ox.print('Ox.Map.resizeMap()');
        var center = self.map.getCenter();
        self.options.height = that.$element.height();
        self.options.width = that.$element.width();
        Ox.print(self.options.width, self.options.height)
        self.$map.css({
            height: self.mapHeight + 'px',
            width: self.options.width + 'px'
        });
        google.maps.event.trigger(self.map, 'resize');
        self.map.setCenter(center);
        self.options.zoombar && self.$zoomInput.options({
            size: self.options.width
        });
        */
        updateFormElements();
        google.maps.event.trigger(self.map, 'resize');
		return that;
    }

    that.zoomToPlace = function() {
	    Ox.print('zoomToPlace')
	    var place = getSelectedPlace();
		place && self.map.fitBounds(place.bounds);
		return that;
    };

    that.zoom = function(value) {
        self.map.setZoom(value);
		return that;
    };

    return that;

};
