// console.debug('map interface loaded');

GeoConst = {
    EARTH_RADIUS: 6378137.0,
    DEG_TO_RAD_SCALE: Math.PI / 180.0,
    RAD_TO_DEG_SCALE: 180.0 / Math.PI
}

geofunc = {

    /*
    	getDegDistance : function(high, low) {
    		return (high>low) ? high-low : high-low+180.0;
    	}
    */

    /**
    * Вычисление окна для заданной высоты и ширины в метрах
    * @param lat Широта
    * @param lng Долгота
    * @param latSize Высота окна в метрах по центральному меридиану
    * @param lngSize Длина окна в метрах по центральной параллели
    */
    getLatLngSizeRange: function(lat, lng, latSize, lngSize) {

        var rlat = 0.5 * latSize * GeoConst.RAD_TO_DEG_SCALE
            / GeoConst.EARTH_RADIUS;
        var rlng = 0.5
            * lngSize
            * GeoConst.RAD_TO_DEG_SCALE
            / (GeoConst.EARTH_RADIUS * Math
                .cos(lat * GeoConst.DEG_TO_RAD_SCALE));

        return {
            south: lat - rlat,
            west: lng - rlng,
            north: lat + rlat,
            east: lng + rlng
        };

    },

    getBaseRangeSize: function(south, west, north, east) {
        var lat = 0.5 * (north + south);
        var lng = 0.5 * (west + east);
        var latSize = GeoConst.DEG_TO_RAD_SCALE * (north - south)
            * GeoConst.EARTH_RADIUS;
        var lngSize = GeoConst.DEG_TO_RAD_SCALE * (east - west)
            * GeoConst.EARTH_RADIUS * Math.cos(lat * GeoConst.DEG_TO_RAD_SCALE);

        return {
            lat: lat,
            lng: lng,
            latSize: latSize,
            lngSize: lngSize
        };
    },

    keys: ["district", "subarea", "area", "subregion", "region", "country"],

    formatToponymObject: function(d) {

        var keys = this.keys;

        var r1 = {}, i, len, t, v;
        for (i = 0, len = keys.length; i < len; i++) {
            t = keys[i];
            if (t in d) {
                v = typeof d[t] == "string" ? d[t] : d[t].name;
                if (t == 'area') {
                    v = v.replace(/^город\s+/gi, "");
                }
                r1[t] = v;
            }
        }

        return r1;

    },

    createGeocoder: function() {
        var geo = new GeocodeControl();
        geo.addGeocodeDriver("google", new GoogleGeocodeDriver());
        geo.addGeocodeDriver("yandex", new YandexGeocodeDriver());
        return geo;
    },

    scaleBounds: function(b, z) {
        var r = (1 / z + 1) * 0.5;
        return {
            south: (r * b.south + (1 - r) * b.north),
            west: (r * b.west + (1 - r) * b.east),
            north: (r * b.north + (1 - r) * b.south),
            east: (r * b.east + (1 - r) * b.west)
        };
    }

}

// -------------------

/*
geo.Range = function(south, west, north, east) {
	this.south = south;
	this.west = west;
	this.north = north;
	this.east = east;
}

geo.Range.prototype = {
	clone : function() { return new geo.Range(this.south, this.west, this.north, this.east); }
}

geo.Point = function(lat, lng) {
	this.lat = lat;
	this.lng = lng;
}

geo.Point.prototype = {
	clone : function() { return new geo.Point(this.lat, this.lng); }
}*/

// -------------------
function MapControl() {
    this.mapDrivers = [];
    this.mapDriverRefs = {}
    this.posChangeHandlers = [];
    this.boundsChangeHandlers = [];
    this.dragEndHandlers = [];
    this.clickHandlers = [];
    this.markerMoveHandlers = [];
    this.markerClickHandlers = [];
    this.markerOverHandlers = [];
    this.viewModeChangeHandlers = [];

    this.markers = [];
    this.markerMap = {};

    this._rangeBounds = [];
    this._rangeBoundMap = {};

    this._areas = [];
    this._areaMap = {};

    this._boundsNotifylock = 0; // Блокировка обработки событий изменения положения карты

    //	this._frame = { lat : ..., lng : ..., latSize : ... (meters), lngSize : ... (meters) }
}

MapControl.prototype = {

    _posChangeHandler: function(lat, lng) {

        if (this._boundsNotifylock) return;

        this._frame.lat = lat;
        this._frame.lng = lng;

        for (var i = 0; i < this.posChangeHandlers.length; i++) {
            var callback = this.posChangeHandlers[i];
            console.debug('call event move to', lat, lng);
            callback.handler.call(callback.scope, lat, lng);
        }

        this._updateViewBounds();

        console.debug('move to', lat, lng);
    },

    _boundsChangeHandler: function(south, west, north, east) {
        if (this._boundsNotifylock) return;

        var frame = geofunc.getBaseRangeSize(south, west, north, east);

        this._frame.lat = frame.lat;
        this._frame.lng = frame.lng;

        this._adjustFrameSize(frame.latSize, frame.lngSize);

        this._updateViewBounds();

        var notifyFrame = geofunc.getLatLngSizeRange(this._frame.lat,
            this._frame.lng, this._frame.latSize, this._frame.lngSize);

        for (var i = 0; i < this.boundsChangeHandlers.length; i++) {
            var callback = this.boundsChangeHandlers[i];
            callback.handler.call(callback.scope, notifyFrame.south,
                notifyFrame.west, notifyFrame.north, notifyFrame.east);
        }
    },

    _dragEndHandler: function() {
        //		console.debug('!!! !!! Drag end');
        for (var i = 0; i < this.dragEndHandlers.length; i++) {
            var callback = this.dragEndHandlers[i];
            callback.handler.call(callback.scope);
        }
    },

    _viewModeChangeHandler: function(mode) {
        console.debug('Start view mode notify');

        this._viewMode = mode;

        for (var i = 0; i < this.viewModeChangeHandlers.length; i++) {
            console.debug('Start view mode notify: notified');
            var callback = this.viewModeChangeHandlers[i];
            callback.handler.call(callback.scope, mode);
        }
    },

    _adjustFrameSize: function(latSize, lngSize) {

        var updateLat = (this._frame.latSize > latSize * 1.1)
            || (this._frame.latSize < latSize * 1.0);

        if (updateLat) {
            console.debug('_adjustFrameSize, update lat', this._frame.latSize,
                'to', latSize);
            this._frame.latSize = latSize * 1.1;
        }

        var updateLng = (this._frame.lngSize > lngSize * 1.1)
            || (this._frame.lngSize < lngSize * 1.0);

        if (updateLng) {
            console.debug('_adjustFrameSize, update lng', this._frame.lngSize,
                'to', lngSize);
            this._frame.lngSize = lngSize * 1.1;
        }

        /*
        		if(this._frame.latSize>latSize) {
        			console.debug('shrink lat',this._frame.latSize,'to',latSize);
        			this._frame.latSize = (latSize>0) ? latSize : 10000;
        		}
        		
        		if(this._frame.latSize*2<latSize) {
        			console.debug('expand lat',this._frame.latSize,'to',latSize);
        			this._frame.latSize = latSize;
        		}
        		
        		if(this._frame.lngSize>lngSize) {
        			console.debug('shrink lng',this._frame.lngSize,'to',lngSize);
        			this._frame.lngSize = (lngSize>0) ? lngSize : 10000;
        		}

        		if(this._frame.lngSize*2<lngSize) {
        			console.debug('expand lng',this._frame.lngSize,'to',lngSize);
        			this._frame.lngSize = lngSize;
        		}
        */
    },

    _clickHandler: function(lat, lng) {
        for (var i = 0; i < this.clickHandlers.length; i++) {
            var callback = this.clickHandlers[i];
            callback.handler.call(callback.scope, lat, lng);
        }
        console.debug('click on', lat, lng);
    },

    _markerMoveHandler: function(name, lat, lng) {
        var marker = this.markerMap[name];
        marker.lat = lat;
        marker.lng = lng;

        for (var i = 0; i < this.markerMoveHandlers.length; i++) {
            var callback = this.markerMoveHandlers[i];
            callback.handler.call(callback.scope, name, lat, lng);
        }

        console.debug('marker moved to', lat, lng);
    },

    _markerClickHandler: function(name) {
        /*	
        		var marker = this.markerMap[name];
        		marker.lat = lat;
        		marker.lng = lng;		
        */
        for (var i = 0; i < this.markerClickHandlers.length; i++) {
            var callback = this.markerClickHandlers[i];
            callback.handler.call(callback.scope, name);
        }

        console.debug('marker clicked', name);
    },

    _markerOverHandler: function(name, over) {

        for (var i = 0; i < this.markerOverHandlers.length; i++) {
            var callback = this.markerOverHandlers[i];
            callback.handler.call(callback.scope, name, over);
        }

        console.debug('marker over', name, over);
    },

    _balloonCloseHandler: function() {
        this._balloon = null;
    },

    addMapDriver: function(name, driver) {
        this.mapDrivers.push(driver);
        this.mapDriverRefs[name] = driver;
        driver.setMapMoveHandler(this, this._posChangeHandler);
        driver.setBoundsChangeHandler(this, this._boundsChangeHandler);
        driver.setViewModeChangeHandler(this, this._viewModeChangeHandler);

        driver.setClickHandler(this, this._clickHandler);

        driver.setMarkerMoveHandler(this, this._markerMoveHandler);
        driver.setMarkerClickHandler(this, this._markerClickHandler);
        driver.setMarkerOverHandler(this, this._markerOverHandler);

        driver.setDragEndHandler(this, this._dragEndHandler);

        driver.setBalloonCloseHandler(this, this._balloonCloseHandler);

        if (this.mapDrivers.length == 1) {
            this.activeDriver = driver;
            this.activeDriverName = name;
            driver.show();
            this._updateMapBounds();
        } else
            driver.hide();
    },

    addPosChangeHandler: function(scope, handler) {
        this.posChangeHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addBoundsChangeHandler: function(scope, handler) {
        this.boundsChangeHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addDragEndHandler: function(scope, handler) {
        this.dragEndHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addClickHandler: function(scope, handler) {
        this.clickHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addMarkerClickHandler: function(scope, handler) {
        this.markerClickHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addMarkerMoveHandler: function(scope, handler) {
        this.markerMoveHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addMarkerOverHandler: function(scope, handler) {
        this.markerOverHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    addViewModeChangeHandler: function(scope, handler) {
        this.viewModeChangeHandlers.push({
            scope: scope,
            handler: handler
        });
    },

    _updateMapBounds: function() {

        if (!this._frame || !this.activeDriver) return;

        this._boundsNotifylock++;

        var bounds = geofunc.getLatLngSizeRange(this._frame.lat,
            this._frame.lng, this._frame.latSize * 0.9, this._frame.lngSize
                * 0.9);

        console.debug('*** _updateMapBounds', bounds);

        this.activeDriver.setBounds(bounds.south, bounds.west, bounds.north,
            bounds.east);

        this._boundsNotifylock--;

        //		var state = this.activeDriver.getViewState();
        //		var frame = geofunc.getBaseRangeSize(state.south, state.west, state.north, state.east);

        //		this._adjustFrameSize(frame.latSize, frame.lngSize);

        //		this._updateViewBounds();
    },

    _updateViewBounds: function() {

        var bounds = geofunc.getLatLngSizeRange(this._frame.lat,
            this._frame.lng, this._frame.latSize, this._frame.lngSize);

        /*
        		this.setArea("view", [
        			bounds.south, bounds.west,
        			bounds.south, bounds.east,
        			bounds.north, bounds.east,
        			bounds.north, bounds.west,
        			bounds.south, bounds.west
        		]);
        */
    },

    setMapFrame: function(lat, lng, latSize, lngSize) {
        this._frame = {
            lat: Number(lat),
            lng: Number(lng),
            latSize: Number(latSize),
            lngSize: Number(lngSize)
        };

        this._updateMapBounds();

        var bounds = geofunc.getLatLngSizeRange(this._frame.lat,
            this._frame.lng, this._frame.latSize, this._frame.lngSize);

        for (var i = 0; i < this.boundsChangeHandlers.length; i++) {
            var callback = this.boundsChangeHandlers[i];
            callback.handler.call(callback.scope, bounds.south, bounds.west,
                bounds.north, bounds.east);
        }
    },

    getMapFrame: function() {
        return (this._frame) ? {
            lat: this._frame.lat,
            lng: this._frame.lng,
            latSize: this._frame.latSize,
            lngSize: this._frame.lngSize
        } : null;
    },

    getMapBounds: function() {
        var frame = this.getMapFrame();
        return (frame) ? geofunc.getLatLngSizeRange(frame.lat, frame.lng,
            frame.latSize, frame.lngSize) : null;
    },

    getMapViewState: function() {
        if (this.activeDriver) {
            return this.activeDriver.getViewState();
        } else {
            return null;
        }
    },

    setMapCenter: function(lat, lng) {
        if (this._frame) {
            this
                .setMapFrame(lat, lng, this._frame.latSize, this._frame.lngSize);
        } else {
            this.setMapFrame(lat, lng, 15000, 20000);
        }
    },

    setMapBounds: function(south, west, north, east) {
        var frame = geofunc.getBaseRangeSize(Number(south), Number(west),
            Number(north), Number(east));
        this.setMapFrame(frame.lat, frame.lng, frame.latSize, frame.lngSize);
    },

    shiftMapCenter: function(lat, lng) {
        if (this.activeDriver) {
            this.activeDriver.shiftCenter(Number(lat), Number(lng));
        } else {
            this.setMapCenter(Number(lat), Number(lng));
        }
    },

    _restoreBounds: function() {
        for (var i = 0; i < this._rangeBounds.length; i++) {
            var bound = this._rangeBounds[i];
            this.activeDriver.setRangeBound(bound.name, bound.south,
                bound.west, bound.north, bound.east, bound.color);
        }
    },

    _restoreAreas: function() {
        for (var i = 0; i < this._areas.length; i++) {
            var area = this._areas[i];
            this.activeDriver.setArea(area.name, area.points);
        }
    },

    _restoreBalloon: function() {
        if (this._balloon) {
            this.activeDriver.showBalloon(this._balloon.content,
                this._balloon.lat, this._balloon.lng);
        } else {
            this.activeDriver.clearBalloon();
        }
    },

    _restoreMarkers: function() {
        for (var i = 0; i < this.markers.length; i++) {
            var marker = this.markers[i];
            this.activeDriver.setupMarker(marker.name, marker.lat, marker.lng,
                marker.options);
        }
    },

    selectMap: function(name) {
        if (name in this.mapDriverRefs) {
            var ref = this.mapDriverRefs[name];
            if (this.activeDriver != ref) {
                this.activeDriver.hide();
                this.activeDriver.clear();

                this.activeDriver = ref;
                this.activeDriverName = name;

                ref.show();

                this._updateMapBounds();
                this._restoreViewMode();

                this._restoreMarkers();
                this._restoreBounds();
                this._restoreAreas();

                this._restoreBalloon();

            }
        }
    },

    getActiveMap: function() {
        return this.activeDriverName;
    },

    setMarker: function(name, lat, lng, moveable) {
        this.setupMarker(name, lat, lng, {
            moveable: (moveable) ? true : false
        });
    },

    removeMarker: function(name) {
        if (name in this.markerMap) {
            var item = this.markerMap[name];
            this.markers.remove(item);
            delete this.markerMap[name];
            if (this.activeDriver) this.activeDriver.removeMarker(name);
        }
    },

    setupMarker: function(name, lat, lng, options) {
        if (name in this.markerMap) {
            var marker = this.markerMap[name];
            marker.lat = Number(lat);
            marker.lng = Number(lng);
            marker.options = options;
        } else {
            var marker = {
                name: name,
                lat: Number(lat),
                lng: Number(lng),
                options: options
            };
            this.markers.push(marker);
            this.markerMap[name] = marker;
        }
        if (this.activeDriver)
            this.activeDriver
                .setupMarker(name, marker.lat, marker.lng, options);
    },

    clear: function() {
        this.markers = [];
        this.markerMap = {};
        this._rangeBounds = [];
        this._rangeBoundMap = {};
        if (this.activeDriver) this.activeDriver.clear();
    },

    clearMarkers: function() {
        this.clear();
    },

    setRangeBound: function(name, south, west, north, east, color) {

        return;

        if (name in this._rangeBoundMap) {
            var bound = this._rangeBoundMap[name];
            bound = {
                south: south,
                west: west,
                north: north,
                east: east,
                color: color
            };
        } else {
            bound = {
                south: south,
                west: west,
                north: north,
                east: east,
                color: color
            };
            this._rangeBounds.push(bound);
            this._rangeBoundMap[name] = bound;
        }
        if (this.activeDriver)
            this.activeDriver.setRangeBound(name, south, west, north, east,
                color);
    },

    setArea: function(name, points) {
        if (name in this._areaMap) {
            var area = this._areaMap[name];
            area.points = points;
        } else {
            area = {
                name: name,
                points: points
            };
            this._areas.push(area);
            this._areaMap[name] = area;
        }
        if (this.activeDriver) this.activeDriver.setArea(name, points);
    },

    _restoreViewMode: function() {
        if (this.activeDriver) {
            this.activeDriver.setViewMode(this.getViewMode());
        }
    },

    setViewMode: function(mode) {
        this._viewMode = mode;
        this._restoreViewMode();
    },

    getViewMode: function() {
        return this._viewMode ? this._viewMode : "normal";
    },

    showBalloon: function(content, lat, lng) {
        if (arguments.length == 1) {
            frame = this.getMapFrame();
            lat = frame.lat;
            lng = frame.lng;
        }
        this._balloon = {
            content: content,
            lat: lat,
            lng: lng
        };
        if (this.activeDriver) {
            this.activeDriver.showBalloon(content, lat, lng);
        }
    },

    clearBalloon: function() {
        this._balloon = null;
        if (this.activeDriver) {
            this.activeDriver.clearBalloon();
        }
    }

}

function GeocodeControl() {
    this.drivers = [];
    this.driverMap = {}
}

function GeocodeResult() {}

GeocodeControl.prototype = {

    addGeocodeDriver: function(name, driver) {
        this.driverMap[name] = driver;
        this.drivers.push(driver);
    },

    geocode: function(lat, lng, scope, callback) {

        console.debug('*** GEOCODE for', lat, lng);

        var result = {
            best: null,
            bestAccuracity: 0,
            bestScore: 0,
            data: [],
            prov: {},
            rest: this.drivers.length,
            bad: 0
        };
        result.prov['yandex'] = {}
        result.prov['google'] = {}
        for (var i = 0; i < this.drivers.length; i++) {
            this.drivers[i].geocode(lat, lng, this, function(status) {
                if (status) {
                    console.debug('GEOCODE VARIANT', $.extend({}, status));

                    var accuracity = 0;
                    var score = 0;

                    if (status.country) {
                        accuracity = 1;
                        score++;
                    }
                    if (status.region) {
                        accuracity = 2;
                        score++;
                    }
                    if (status.subregion) {
                        accuracity = 3;
                        score++;
                    }
                    if (status.area) {
                        accuracity = 4;
                        score++;
                    }
                    if (status.subarea) {
                        accuracity = 5;
                        score++;
                    }
                    if (status.district) {
                        accuracity = 6;
                        score++;
                    }

                    status.accuracity = accuracity;
                    status.score = score;

                    result.data.push(status);

                    result.prov[status.copyright] = status;

                    if (result.bestAccuracity < accuracity
                        || (result.bestAccuracity == accuracity && result.bestScore < score)) {
                        result.bestAccuracity = accuracity;
                        result.bestScore = score;
                        result.best = status;
                    }
                } else {
                    result.bad++;
                }
                result.rest--;
                if (result.rest == 0) {

                    //					console.log('*** GEORESULT-G', $.extend({}, result.prov['google']));
                    //					console.log('*** GEORESULT-Y', $.extend({}, result.prov['yandex']));

                    if (result.prov['yandex'] && result.prov['yandex'].area) {

                        if (result.prov['yandex'].area
                            && result.prov['yandex'].area.name == 'Екатеринбург') {

                            console.debug('CHECK EK');

                            if (!result.prov['yandex'].subregion)
                                delete result.prov['google'].subregion;
                            //							else
                            //								console.log('EK OK');
                        }

                        if (result.prov['yandex'].area
                            && result.prov['yandex'].area.name == 'Нижний Новгород') {

                            console.debug('CHECK NN');

                            if (!result.prov['yandex'].subregion)
                                delete result.prov['google'].subarea;
                            //							else
                            //								console.log('NN OK');
                        }

                    }

                    if ('district' in result.prov['google'])
                        delete result.prov['yandex'].district;
                    if ('locality' in result.prov['google'])
                        delete result.prov['yandex'].locality;
                    if ('subregion' in result.prov['google'])
                        delete result.prov['yandex'].subregion;
                    if ('region' in result.prov['google'])
                        delete result.prov['yandex'].region;
                    if ('country' in result.prov['google'])
                        delete result.prov['yandex'].country;

                    //					if(status.subregion) { accuracity = 3; score++; }
                    //					if(status.area) { accuracity = 4; score++; }
                    //					if(status.subarea) { accuracity = 5; score++; }
                    //					if(status.district) { accuracity = 6; score++; }

                    $.extend(result.prov['google'], result.prov['yandex']);
                    $.extend(result.prov['yandex'], result.prov['google']);

                    console.debug('GEOCODE BEST', status);
                    callback.call(scope, result);
                }
            });
        }
        return result;
    },

    seek: function(address, scope, callback) {
        var result = {
            best: null,
            bestAccuracity: 0,
            bestScore: 0,
            data: [],
            rest: this.drivers.length,
            bad: 0
        };
        for (var i = 0; i < this.drivers.length; i++) {
            this.drivers[i].seek(address, this, function(status) {
                if (status) {
                    console.debug('GEOCODE VARIANT', $.extend({}, status));

                    var accuracity = 0;
                    var score = 0;

                    if (status.country) {
                        accuracity = 1;
                        score++;
                    }
                    if (status.region) {
                        accuracity = 2;
                        score++;
                    }
                    if (status.subregion) {
                        accuracity = 3;
                        score++;
                    }
                    if (status.area) {
                        accuracity = 4;
                        score++;
                    }
                    if (status.subarea) {
                        accuracity = 5;
                        score++;
                    }
                    if (status.district) {
                        accuracity = 6;
                        score++;
                    }
                    if (status.building) {
                        accuracity = 8;
                        score++;
                    }

                    status.accuracity = accuracity;
                    status.score = score;

                    result.data.push(status);
                    if (!result.best
                        || result.bestAccuracity < accuracity
                        || (result.bestAccuracity == accuracity && result.bestScore < score)) {
                        result.bestAccuracity = accuracity;
                        result.bestScore = score;
                        result.best = status;
                    }
                } else {
                    result.bad++;
                }
                result.rest--;
                if (result.rest == 0) callback.call(scope, result);
            });
        }
        return result;
    },

    seek2: function(address, frame, scope, callback) {
        var result = {
            best: null,
            bestAccuracity: 0,
            bestScore: 0,
            data: [],
            rest: this.drivers.length,
            bad: 0
        };
        for (var i = 0; i < this.drivers.length; i++) {
            this.drivers[i].seek2(address, frame, this, function(status) {
                if (status) {
                    console.debug('GEOCODE VARIANT', $.extend({}, status));

                    var accuracity = 0;
                    var score = 0;

                    if (status.country) {
                        accuracity = 1;
                        score++;
                    }
                    if (status.region) {
                        accuracity = 2;
                        score++;
                    }
                    if (status.subregion) {
                        accuracity = 3;
                        score++;
                    }
                    if (status.area) {
                        accuracity = 4;
                        score++;
                    }
                    if (status.subarea) {
                        accuracity = 5;
                        score++;
                    }
                    if (status.district) {
                        accuracity = 6;
                        score++;
                    }
                    if (status.box || status.location) {
                        accuracity = 7;
                        score++;
                    }
                    if (status.building) {
                        accuracity = 8;
                        score++;
                    }

                    status.accuracity = accuracity;
                    status.score = score;

                    result.data.push(status);
                    if (!result.best
                        || result.bestAccuracity < accuracity
                        || (result.bestAccuracity == accuracity && result.bestScore < score)) {
                        result.bestAccuracity = accuracity;
                        result.bestScore = score;
                        result.best = status;
                    }
                } else {
                    result.bad++;
                }
                result.rest--;
                if (result.rest == 0) callback.call(scope, result);
            });
        }
        return result;
    }

}

function GoogleGeocodeDriver() {}

GoogleGeocodeDriver.prototype = {

    _fillDetails: function(result, details) {

        var addr = details;

        if (addr.Country) {
            addr = addr.Country;
            result.country = {
                name: addr.CountryName
            };
        }

        if (addr.AdministrativeArea) {
            addr = addr.AdministrativeArea;
            result.region = {
                name: addr.AdministrativeAreaName
            };
        }

        if (addr.SubAdministrativeArea) {
            addr = addr.SubAdministrativeArea;
            result.subregion = {
                name: addr.SubAdministrativeAreaName
            };
        }

        if (addr.Locality) {
            addr = addr.Locality;
            result.area = {
                name: addr.LocalityName
            };
        }

        if (addr.DependentLocality) {
            addr = addr.DependentLocality;
            result.district = {
                name: addr.DependentLocalityName
            };
        }

    },

    _getLastLocation: function(details) {

        var addr = details;
        var target = null;
        var name = null;

        if (addr.Country) {
            addr = addr.Country;
            target = 'country';
            name = addr.CountryName;
        }

        if (addr.AdministrativeArea) {
            addr = addr.AdministrativeArea;
            target = 'region';
            name = addr.AdministrativeAreaName;
        }

        if (addr.SubAdministrativeArea) {
            addr = addr.SubAdministrativeArea;
            target = 'subregion';
            name = addr.SubAdministrativeAreaName;
        }

        if (addr.Locality) {
            addr = addr.Locality;
            target = 'area';
            name = addr.LocalityName;
        }

        if (addr.DependentLocality) {
            addr = addr.DependentLocality;
            target = 'district';
            name = addr.DependentLocalityName;
        }

        return {
            target: target,
            name: name,
            addr: addr
        };

    },

    _updateLocationDetails: function(result, addr, details, address) {

        //			console.debug('addr',addr,'result',result);

        var target = result[addr.target];

        if (target && addr.name == target.name) {
            target.box = {
                south: details.south,
                west: details.west,
                north: details.north,
                east: details.east
            };
            target.address = address;
        }

    },

    _translate2: function(types, history) {

        var dict1 = {
            'country': 'country',
            'administrative_area_level_1': 'region',
            'administrative_area_level_2': 'subregion',
            'locality': 'area',
            'sublocality': 'district'
        }

        var dict2 = {
            'country': 'country',
            'locality': 'area',
            'administrative_area_level_1': 'subarea',
            'administrative_area_level_2': 'subarea',
            'sublocality': 'district'
        }

        for (var i = 0; i < types.length; i++) {

            var result;

            if ('area' in history) {
                var result = dict2[types[i]];
            } else {
                var result = dict1[types[i]];
            }

            if (result) return result;
        }

        return null;
    },

    _translate: function(types) {

        var dict = {
            'country': 'country',
            'administrative_area_level_1': 'region',
            'administrative_area_level_2': 'subregion',
            'locality': 'area',
            'sublocality': 'district'
        }

        for (var i = 0; i < types.length; i++) {
            var result = dict[types[i]];
            if (result) return result;
        }

        return null;
    },

    _translateseek: function(types, history) {

        var dict1 = {
            'country': 'country',
            'administrative_area_level_1': 'region',
            'administrative_area_level_2': 'subregion',
            'locality': 'area',
            'sublocality': 'district',
            'street_address': 'building'
        }

        var dict2 = {
            'country': 'country',
            'locality': 'area',
            'administrative_area_level_1': 'subarea',
            'administrative_area_level_2': 'subarea',
            'sublocality': 'district',
            'street_address': 'building'
        }

        for (var i = 0; i < types.length; i++) {

            var result;

            if ('area' in history) {
                var result = dict2[types[i]];
            } else {
                var result = dict1[types[i]];
            }

            if (result) return result;
        }

        return null;
    },

    _translatespec: function(types) {

        var dict = {
            'country': 'country',
            'administrative_area_level_1': 'region',
            'locality': 'area',
            'administrative_area_level_2': 'subarea',
            'sublocality': 'district'
        }

        for (var i = 0; i < types.length; i++) {
            var result = dict[types[i]];
            if (result) return result;
        }

        return null;
    },

    geocode: function(lat, lng, scope, callback) {

        var geocoder = new google.maps.Geocoder();

        var driver = this;

        geocoder.geocode({
            latLng: new google.maps.LatLng(lat, lng)
        },

        function(data, status) {

            console.debug('try to render google', data, status);

            if (status != google.maps.GeocoderStatus.OK) {
                callback.call(scope, null);
                return;
            }

            var first = data[0];

            var result = {
                copyright: 'google',
                address: first.formatted_address
            };

            var history = {};

            for (var i = data.length - 1; i >= 0; i--) {
                var item = data[i];
                var member = driver._translate2(item.types, history);

                if (member) {
                    history[member] = i;

                    var target = {
                        address: item.formatted_address,
                        name: item.address_components[0].long_name
                    };
                    if (item.geometry && item.geometry.bounds) {
                        var bounds = item.geometry.bounds;
                        var ne = bounds.getNorthEast();
                        var sw = bounds.getSouthWest();
                        target.box = {
                            south: sw.lat(),
                            west: sw.lng(),
                            north: ne.lat(),
                            east: ne.lng()
                        };
                    }
                    result[member] = target;
                }
            }

            /*
            				var addr = first.address_components;

            				var history = {};

            				for(var i=addr.length-1; i>=0; i--) {
            					var item = addr[i];
            					var member = driver._translate2(item.types, history);

            					if(member in result) {
            						history[member] = i;
            						result[member].name = item.long_name;
            					}
            				}
            */
            if ('region' in result && 'area' in result) {

                var specArea = (result.region.name == result.area.name)
                    || (result.area.name == 'город Москва')
                    || (result.area.name == 'город Санкт-Петербург');

                if (specArea) {
                    delete result.region;
                }
            }

            callback.call(scope, result);

        });
    },

    seek: function(address, scope, callback) {

        var geocoder = new google.maps.Geocoder();

        var driver = this;

        geocoder.geocode({
            address: address
        }, function(data, status) {

            console.debug('try to render google (seek)', data, status);

            if (status != google.maps.GeocoderStatus.OK) {
                callback.call(scope, null);
                return;
            }

            var first = data[0];

            var result = {
                copyright: 'google',
                address: first.formatted_address
            };

            var box = first.geometry.viewport;

            if (box) {
                var ne = box.getNorthEast();
                var sw = box.getSouthWest();
                result.box = {
                    north: ne.lat(),
                    east: ne.lng(),
                    south: sw.lat(),
                    west: sw.lng()
                };
            }

            if (first.geometry.location) {
                result.location = {
                    lat: first.geometry.location.lat(),
                    lng: first.geometry.location.lng()
                };
            }

            var history = {};

            for (var i = data.length - 1; i >= 0; i--) {
                var item = data[i];
                var member = driver._translateseek(item.types, history);

                if (member) {
                    history[member] = i;

                    var target = {
                        address: item.formatted_address,
                        name: item.address_components[0].long_name
                    };
                    if (item.geometry && item.geometry.bounds) {
                        var bounds = item.geometry.bounds;
                        var ne = bounds.getNorthEast();
                        var sw = bounds.getSouthWest();
                        target.box = {
                            south: sw.lat(),
                            west: sw.lng(),
                            north: ne.lat(),
                            east: ne.lng()
                        };
                    }
                    result[member] = target;
                }
            }

            /*
            				var addr = first.address_components;

            				var history = {};

            				for(var i=addr.length-1; i>=0; i--) {
            					var item = addr[i];
            					var member = driver._translate2(item.types, history);

            					if(member in result) {
            						history[member] = i;
            						result[member].name = item.long_name;
            					}
            				}
            */
            if ('region' in result && 'area' in result
                && result.region.name == result.area.name) {
                delete result.region;
            }

            callback.call(scope, result);

        });

    },

    seek2: function(address, frame, scope, callback) {

        var bounds = new google.maps.LatLngBounds(new google.maps.LatLng(
                frame.south, frame.west), new google.maps.LatLng(frame.north,
                frame.east));

        var geocoder = new google.maps.Geocoder();

        var driver = this;

        geocoder.geocode({
            address: address,
            bounds: bounds
        }, function(data, status) {

            console.debug('try to render google (seek2)', data, status);

            if (status != google.maps.GeocoderStatus.OK) {
                callback.call(scope, null);
                return;
            }

            var first = data[0];

            var result = {
                copyright: 'google',
                address: first.formatted_address
            };

            var box = first.geometry.viewport;

            if (box) {
                var ne = box.getNorthEast();
                var sw = box.getSouthWest();
                result.box = {
                    north: ne.lat(),
                    east: ne.lng(),
                    south: sw.lat(),
                    west: sw.lng()
                };
            }

            if (first.geometry.location) {
                result.location = {
                    lat: first.geometry.location.lat(),
                    lng: first.geometry.location.lng()
                };
            }

            var history = {};

            for (var i = data.length - 1; i >= 0; i--) {
                var item = data[i];
                var member = driver._translateseek(item.types, history);

                if (member) {
                    history[member] = i;

                    var target = {
                        address: item.formatted_address,
                        name: item.address_components[0].long_name
                    };
                    if (item.geometry && item.geometry.bounds) {
                        var bounds = item.geometry.bounds;
                        var ne = bounds.getNorthEast();
                        var sw = bounds.getSouthWest();
                        target.box = {
                            south: sw.lat(),
                            west: sw.lng(),
                            north: ne.lat(),
                            east: ne.lng()
                        };
                    }
                    result[member] = target;
                }
            }

            /*
            				var addr = first.address_components;

            				var history = {};

            				for(var i=addr.length-1; i>=0; i--) {
            					var item = addr[i];
            					var member = driver._translate2(item.types, history);

            					if(member in result) {
            						history[member] = i;
            						result[member].name = item.long_name;
            					}
            				}
            */
            if ('region' in result && 'area' in result
                && result.region.name == result.area.name) {
                delete result.region;
            }

            callback.call(scope, result);

        });

    }

}

function YandexGeocodeDriver() {}

YandexGeocodeDriver.prototype = {

    _fillDetails: function(result, details) {

        var addr = details;

        if (addr.Country) {
            addr = addr.Country;
            result.country = {
                name: addr.CountryName
            };
        }

        if (addr.AdministrativeArea) {
            addr = addr.AdministrativeArea;
            result.region = {
                name: addr.AdministrativeAreaName
            };
        }

        if (addr.SubAdministrativeArea) {
            addr = addr.SubAdministrativeArea;
            result.subregion = {
                name: addr.SubAdministrativeAreaName
            };
        }

        if (addr.Locality) {
            addr = addr.Locality;
            result.area = {
                name: addr.LocalityName
            };
        }

        if (addr.Premise) {
            addr = addr.Premise;
            if (!result.area) {
                result.area = {
                    name: addr.PremiseName
                };
            }
        }

        if (addr.DependentLocality) {
            addr = addr.DependentLocality;
            result.district = {
                name: addr.DependentLocalityName
            };
        }

    },

    _getLastLocation: function(details) {

        var addr = details;
        var target = null;
        var name = null;

        if (addr.Country) {
            addr = addr.Country;
            target = 'country';
            name = addr.CountryName;
        }

        if (addr.AdministrativeArea) {
            addr = addr.AdministrativeArea;
            target = 'region';
            name = addr.AdministrativeAreaName;
        }

        if (addr.SubAdministrativeArea) {
            addr = addr.SubAdministrativeArea;
            target = 'subregion';
            name = addr.Country;
        }

        if (addr.Locality) {
            addr = addr.Locality;

            target = 'area';
            name = addr.LocalityName;
        }

        if (addr.Premise) {
            addr = addr.Premise;
            target = 'area';
            name = addr.PremiseName;
        }

        if (addr.DependentLocality) {
            addr = addr.DependentLocality;
            target = 'district';
            name = addr.DependentLocalityName;
        }

        return {
            target: target,
            name: name,
            addr: addr
        };

    },

    _updateLocationDetails: function(result, addr, bounds, text) {

        //			console.debug('addr',addr,'result',result);

        var target = result[addr.target];

        if (target && addr.name == target.name) {
            target.box = {
                south: bounds.getBottom(),
                west: bounds.getLeft(),
                north: bounds.getTop(),
                east: bounds.getRight()
            };
            target.address = text;
        }

    },

    geocode: function(lat, lng, scope, callback) {

        var geocoder = new YMaps.Geocoder(new YMaps.GeoPoint(lng, lat));

        var driver = this;

        YMaps.Events.observe(geocoder, geocoder.Events.Load, function() {
            if (!this.length()) {
                callback.call(scope, null);
                return;
            }

            console.debug('*** YANDEX', this.get(0).AddressDetails);

            var result = {
                copyright: 'yandex',
                address: this.get(0).text
            };

            driver._fillDetails(result, this.get(0).AddressDetails);

            for (var i = 0; i < this.length(); i++) {
                var addr = this.get(i);
                if (addr.kind == "district" || addr.kind == "locality"
                    || addr.kind == "province" || addr.kind == "country") {
                    driver._updateLocationDetails(result, driver
                            ._getLastLocation(addr.AddressDetails), addr
                            .getBounds(), addr.text);
                }
            }

            yagc = this;
            yagr = result;

            callback.call(scope, result);

        })

        YMaps.Events.observe(geocoder, geocoder.Events.Fault, function(
            geocoder, errorMessage) {
            callback.call(scope, null);
        });

    },

    seek: function(address, scope, callback) {

        var geocoder = new YMaps.Geocoder(address);

        var driver = this;

        YMaps.Events.observe(geocoder, geocoder.Events.Load, function() {
            if (!this.length()) {
                callback.call(scope, null);
                return;
            }

            var result = {
                copyright: 'yandex',
                address: this.get(0).text
            };

            var first = this.get(0);

            if (first.precision == 'exact') {
                var pnt = first.getGeoPoint();
                result.location = {
                    lat: pnt.getLat(),
                    lng: pnt.getLng()
                };
            }

            driver._fillDetails(result, this.get(0).AddressDetails);

            /*			
            			for(var i=0; i<this.length(); i++) {
            				var addr = this.get(i);
            				if(addr.kind=="district" || addr.kind=="locality" || addr.kind=="province" || addr.kind=="country") {
            					driver._updateLocationDetails(result, driver._getLastLocation(addr.AddressDetails), addr.getBounds(), addr.text);
            				}
            			}
            */
            yagc = this;
            yagr = result;

            console.debug('Seek result', this);

            //            callback.call(scope, result);

            callback.call(scope, null);

        })

        YMaps.Events.observe(geocoder, geocoder.Events.Fault, function(
            geocoder, errorMessage) {
            callback.call(scope, null);
        });

    },

    seek2: function(address, frame, scope, callback) {
        this.seek(address, scope, callback);
    }

}

/* ------------------------- Controls ------------------------------- */

myterra.CommonMap = function(options) {

    this._options = $.extend({
        parent: null,
        switchBlock: null,
        mapBlock: null,
        restoreDriver: true,
        storeDriver: true,
        restoreFrame: true,
        storeFrame: true,
        restoreViewMode: true,
        storeViewMode: true
    }, options);

    this._initialize();

}

myterra.CommonMap.prototype = {

    _initialize: function() {

        this.map = (this._options.map) ? this._options.map : new MapControl();

        var switchBlock = (this._options.switchBlock)
            ? this._options.switchBlock
            : $('<div class="map_switch_block"></div>')
                .appendTo(this._options.parent);

        var scope = this;

        var ysw = $('<div class="map_switch map_switch_yandex"></div>')
            .appendTo(switchBlock).click(function() {
                scope._selectDriver('ymap');
            });
        var gsw = $('<div class="map_switch map_switch_google"></div>')
            .appendTo(switchBlock).click(function() {
                scope._selectDriver('gmap');
            });

        this._mapBlock = (this._options.mapBlock) ? this._options.mapBlock
            : $('<div class="map_area" style="position:relative"></div>')
                .appendTo(this._options.parent);

        this._chctl = new myterra.Crosshair(this._mapBlock);
        this._chctl.update();
        this._chctl.showCrosshair();

        if (!this._options.mapBlock && !this._options.switchBlock) {
            var blockHeight = this._options.parent.innerHeight()
                - switchBlock.outerHeight();
            console.debug('block height', blockHeight);
            this._mapBlock.height(blockHeight);
        } else if (!this._options.mapBlock) {
            var blockHeight = this._options.parent.innerHeight();
            console.debug('block height', blockHeight);
            this._mapBlock.height(blockHeight);
        }

        this._ymap = $('<div></div>').appendTo(this._mapBlock);
        this._gmap = $('<div></div>').appendTo(this._mapBlock);

        this._adjustMapSize();

        this.map.addMapDriver('gmap', new GoogleMapV3Interface(this._gmap[0]));
        this.map.addMapDriver('ymap', new YandexMapInterface(this._ymap[0]));

        if (this._options.restoreDriver) this._restoreDriver();
        if (this._options.restoreFrame) this._restoreFrame();
        if (this._options.restoreViewMode) this._restoreViewMode();

        if (this._options.storeFrame)
            this.map.addBoundsChangeHandler(this, function() {
                this._saveFrame();
            });
        if (this._options.storeViewMode)
            this.map.addViewModeChangeHandler(this, function() {
                this._saveViewMode();
            });

    },

    _adjustMapSize: function() {
        var height = this._mapBlock.innerHeight();
        var width = this._mapBlock.innerWidth();

        console.debug('adjust map size', width, height);

        this._ymap.height(height).width(width);
        this._gmap.height(height).width(width);
    },

    _selectDriver: function(name) {
        this.map.selectMap(name);
        if (this._options.storeDriver)
            myterra.setAttr('myterra.CommonMap.driver', name);
    },

    _restoreDriver: function() {
        var driver = myterra.getAttr('myterra.CommonMap.driver');
        if (driver) {
            console.debug('Restore map driver', driver);
            this.map.selectMap(driver);
        }
    },

    _saveFrame: function() {
        var frame = this.map.getMapFrame();
        myterra.setAttr('myterra.CommonMap.frame', frame);
    },

    _restoreFrame: function() {
        var frame = myterra.getAttr('myterra.CommonMap.frame');

        if (frame) {
            console.debug('Restore map frame', frame);
            this.map.setMapFrame(frame.lat, frame.lng, frame.latSize,
                frame.lngSize);
        }
    },

    _saveViewMode: function() {
        var mode = this.map.getViewMode();
        myterra.setAttr('myterra.CommonMap.mode', mode);
    },

    _restoreViewMode: function() {
        var mode = myterra.getAttr('myterra.CommonMap.mode');
        if (mode) this.map.setViewMode(mode);
    },

    switchToYandex: function() {
        this._selectDriver('ymap');
    },

    switchToGoogle: function() {
        this._selectDriver('gmap');
    }

}

myterra.Crosshair = function(e) {
    this._e = e;

    this._top = $(this._rectHandlerTemplate).hide().appendTo(e);
    this._bottom = $(this._rectHandlerTemplate).hide().appendTo(e);
    this._left = $(this._rectHandlerTemplate).hide().appendTo(e);
    this._right = $(this._rectHandlerTemplate).hide().appendTo(e);

    this._crosshair = $(this._crosshairTemplate).hide().appendTo(e);

    var scope = this;

    var onHandler = function() {
        scope.showCrosshair();
    };
    var offHandler = function() {
        scope.hideCrosshair();
    };

    this._top.mouseover(onHandler);
    this._left.mouseover(onHandler);
    this._bottom.mouseover(onHandler);
    this._right.mouseover(onHandler);

    this._crosshair.mouseover(offHandler);

}

myterra.Crosshair.prototype = {

    _rectHandlerTemplate: '<div class="crosshairspace" style="position:absolute; padding: 0px; margin: 0px; z-index: 9999"></div>',

    _crosshairTemplate: '<div class="crosshair" style="position:absolute; background-repeat: no-repeat; width: 13px; height: 13px;z-index: 9999; padding: 0px; margin: 0px; background-image: url(\'http://static.streetjournal.org/images/crosshair.gif\')"></div>',

    update: function() {
        var w = this._e.innerWidth();
        var h = this._e.innerHeight();
        var cw = this._crosshair.width();
        var ch = this._crosshair.height();
        var cx = (w - cw) * 0.5;
        var cy = (h - ch) * 0.5;

        this._top.css('left', 0);
        this._top.css('top', 0);
        this._top.css('width', w);
        this._top.css('height', cy);

        this._left.css('left', 0);
        this._left.css('top', cy);
        this._left.css('width', cx);
        this._left.css('height', ch);

        this._right.css('right', 0);
        this._right.css('top', cy);
        this._right.css('width', cx);
        this._right.css('height', ch);

        this._bottom.css('left', 0);
        this._bottom.css('bottom', 0);
        this._bottom.css('width', w);
        this._bottom.css('height', cy);

        this._crosshair.css('left', cx);
        this._crosshair.css('top', cy);

    },

    showCrosshair: function() {
        this._top.hide();
        this._bottom.hide();
        this._left.hide();
        this._right.hide();
        this._crosshair.show();
    },

    hideCrosshair: function() {
        this._top.show();
        this._bottom.show();
        this._left.show();
        this._right.show();
        this._crosshair.hide();
    }

}

