// vim: et:ts=4:sw=4:sts=4:ft=js /*@ Ox.Map Basic map object # DESCRIPTION -------------------------------------------------------------- Ox.Map is a wrapper around the Google Maps API. # USAGE -------------------------------------------------------------------- () -> Map object (options) -> Map object (options, self) -> Map object # ARGUMENTS ---------------------------------------------------------------- options options clickable If true, clicking on the map finds a place editable If true, places are editable findPlaceholder Placeholder text for the find input element maxMarkers Maximum number of markers to be displayed places <[o]|f|null> Array of, or function that returns, place objects countryCode ISO 3166 country code east Longitude of the eastern boundary in degrees editable If true, the place is editable geoname Geoname (like "Paris, Île-de-France, France") lat Latitude in degrees lng Longitude in degrees markerColor CSS color of the place marker markerSize size of the place marker in px name Name (like "Paris") north Latitude of the northern boundary in degrees south Latitude of the southern boundary in degrees type Type (like "city" or "country") west Longitude of the western boundary in degrees selected Id of the selected place showControls If true, show controls showLabels If true, show labels on the map showTypes If true, color markers according to place type statusbar If true, the map has a statusbar toolbar If true, the map has a toolbar self Shared private variable # EVENTS ------------------------------------------------------------------- addplace Fires when a place has been added place Place object editplace Fires when a place has been edited place Place object geocode Fires when a google geocode request returns latLng Query coordinates, or undefined lat latitude lng longitude address Query string, or undefined results <[o]> Google Maps geocode results address_components <[o]> Address components long_name Long name short_name Short name types <[s]> Types (like "country" or "political") formatted_address Formatted address geometry Geometry bounds Bounds northEast North-east corner lat Latitude lng Longitude southWest South-west corner lat Latitude lng Longitude location Location lat Latitude lng Longitude location_type Location type (like "APPROXIMATE") viewport Viewport northEast North-east corner lat Latitude lng Longitude southWest South-west corner lat Latitude lng Longitude types <[s]> Types (like "country" or "political") # EXAMPLES ----------------------------------------------------------------- > 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', maxMarkers: 100, places: null, selected: null, showControls: false, showLabels: false, showTypes: false, 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.isAsync = Ox.isFunction(self.options.places); self.mapHeight = getMapHeight(); self.minZoom = getMinZoom(); self.placeKeys = [ 'id', 'name', 'alternativeNames', 'geoname', 'countryCode', 'type', 'lat', 'lng', 'south', 'west', 'north', 'east', 'area', 'editable' ], 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.$select = Ox.Select({ items: [ {id: 'new Place', title: 'New Place...', keyboard: 'n'}, {}, {id: 'toggleLabels', title: 'Show Labels', keyboard: 'l', checked: self.options.showLabels}, {id: 'toggleControls', title: 'Show Controls', keyboard: 'c', checked: self.options.showControls}, ], min: 0, max: 2, title: 'Options...', width: 96 }) .css({float: 'left', margin: '4px'}) .appendTo(self.$toolbar); /* self.$labelsButton = new Ox.Checkbox({ title: 'Labels', width: 64 }) .css({float: 'left', margin: '4px'}) .bindEvent({ change: 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.$placeFlag = $('') .addClass('OxFlag') .attr({ src: Ox.PATH + 'Ox.Geo/png/icons/16/NTHH.png' }) .css({float: 'left', margin: '4px 2px 4px 4px'}) .appendTo(self.$statusbar.$element); self.$placeNameInput = new Ox.Input({ placeholder: 'Name', width: 96 }) //.css({position: 'absolute', left: 4, top: 4}) .css({float: 'left', margin: '4px 2px 4px 2px'}) .appendTo(self.$statusbar); self.$placeGeonameInput = new Ox.Input({ placeholder: 'Geoname', width: 96 }) //.css({position: 'absolute', left: 104, top: 4}) .css({float: 'left', margin: '4px 2px 4px 2px'}) .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', }) }; Ox.forEach(self.$navigationButtons, function($button) { $button.attr({ src: $button.attr('src').replace('/classic/', '/modern/') }); }); self.$scaleLabel = new Ox.Label({ textAlign: 'center', title: '...' }) .addClass('OxMapLabel') .css({right: '4px', top: '4px'}); if (!self.isAsync) { self.options.places.forEach(function(place) { if (Ox.isUndefined(place.id)) { place.id = Ox.encodeBase32(Ox.uid()); } }); } 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.encodeBase32(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(data) { var place = getSelectedPlace(), country = Ox.getCountryByGeoname(place.geoname); Ox.extend(place, data); self.options.selected = place.id; place.countryCode = country ? country.code : ''; Ox.print('addP2P', data, place); 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) { if (self.options.clickable/* && !editing()*/) { getPlaceByLatLng(event.latLng, self.map.getBounds(), function(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 getMapBounds(callback) { // get initial map bounds var mapBounds; if (!self.isAsync) { self.options.places.forEach(function(place, i) { var bounds = getBounds(place); mapBounds = i == 0 ? bounds : mapBounds.union(bounds); }); callback(mapBounds); } else { self.options.places({}, function(result) { callback(getBounds(result.data.area)); }); } function getBounds(place) { return new google.maps.LatLngBounds( new google.maps.LatLng(place.south, place.west), new google.maps.LatLng(place.north, place.east) ); } } function getMapHeight() { return self.options.height - self.options.statusbar * 24 - self.options.toolbar * 24 - self.options.zoombar * 16; } function getMapType() { return self.options.showLabels ? 'HYBRID' : 'SATELLITE' } function getMaxZoom(point, callback) { if (arguments.length == 1) { callback = point; point = self.map.getCenter(); } Ox.print('CALLING ZOOM SERVICE', point.lat(), point.lng()) self.maxZoomService.getMaxZoomAtLatLng(point, function(data) { Ox.print('ZOOM SERVICE', data.status, data.zoom) callback(data.status == 'OK' ? data.zoom : null); }); } 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 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() { getMapBounds(function(mapBounds) { Ox.print('init', mapBounds.getSouthWest(), mapBounds.getNorthEast(), mapBounds.getCenter()) updateFormElements(); self.elevationService = new google.maps.ElevationService(); self.geocoder = new google.maps.Geocoder(); self.maxZoomService = new google.maps.MaxZoomService(); 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); self.places = []; if (!self.isAsync) { self.options.places.forEach(function(place, i) { self.places[i] = new Ox.MapPlace(Ox.extend({ map: that }, place)); }); } 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) { var bounds = self.map.getBounds(), southWest = bounds.getSouthWest(), northEast = bounds.getNorthEast(), south = southWest.lat(), west = southWest.lng(), north = northEast.lat(), east = northEast.lng(), crossesDateline = west > east; if (!self.isAsync) { self.places.sort(function(a, b) { var sort = { a: a.selected ? Infinity : bounds.contains(a.center) ? a.area : -Infinity, b: b.selected ? Infinity : bounds.contains(b.center) ? b.area : -Infinity, }; return sort.b - sort.a; }).forEach(function(place, i) { if (i < self.options.maxMarkers && !place.visible) { place.add(); } else if (i >= self.options.maxMarkers && place.visible) { place.remove(); } }); } else { Ox.print('QUERY', { conditions: Ox.merge([ {key: 'lat', value: [south, north], operator: '-'} ], !crossesDateline ? [ {key: 'lng', value: [west, east], operator: '-'} ] : [ { conditions: [ {key: 'lng', value: west, operator: '<'}, {key: 'lng', value: east, operator: '>'} ], operator: '|' } ]), operator: '&' }); self.options.places({ keys: self.placeKeys, query: { conditions: Ox.merge([ {key: 'lat', value: [south, north], operator: '-'} ], !crossesDateline ? [ {key: 'lng', value: [west, east], operator: '-'} ] : [ {key: 'lng', value: [east, west], operator: '!-'} ]), operator: '&' }, range: [0, self.options.maxMarkers], sort: [{key: 'area', operator: '-'}] }, function(result) { Ox.print('RESULT', result) var ids = []; result.data.items.forEach(function(item, i) { var place = getPlaceById(item.id); if (!place) { self.places.push( new Ox.MapPlace(Ox.extend({ map: that }, item)).add() ); } else if (!place.visible) { place.add(); } ids.push(item.id); }); self.places.forEach(function(place) { if ( !place.selected && place.visible && ids.indexOf(place.id) == -1 ) { place.remove(); } }); }); } self.boundsChanged = false; } if (self.centerChanged) { getMaxZoom(function(zoom) { if (zoom && 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.encodeBase32(Ox.uid()), map: that, name: data.formatted_address.split(', ')[0], north: bounds.getNorthEast().lat(), south: bounds.getSouthWest().lat(), type: getType(data.address_components[0].types), 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(', '); } function getType(types) { Ox.print('getType', types) // see http://code.google.com/apis/maps/documentation/javascript/services.html#GeocodingAddressTypes var strings = { 'country': ['country'], 'region': ['administrative_area'], 'city': ['locality'], 'borough': ['neighborhood', 'sublocality'], 'street': [ 'intersection', 'route', 'street_address', 'street_number' ], 'premise': [ 'airport', 'establishment', 'park', 'premise', 'subpremise' ], 'feature': ['natural_feature'] }, type = 'other'; Ox.forEach(strings, function(values, key) { Ox.forEach(values, function(value) { if (find(value)) { type = key; return false; } }); return type == 'other'; }); return type; function find(type) { var ret; Ox.forEach(types, function(v) { ret = Ox.startsWith(v, type); return !ret; }); return ret; } } 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() { var place = getSelectedPlace(); place.id = '_' + place.id; self.options.selected = place.id; //Ox.print('removePlace', Ox.getObjectById(self.places, place.id)) self.places.splice(Ox.getPositionById(self.places, place.id), 1); self.resultPlace && self.resultPlace.remove(); self.resultPlace = place; place.marker.update(); } 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) { // id can be null (deselect) var place, selected = getSelectedMarker(); Ox.print('Ox.Map selectPlace()', id, selected); if (id != selected) { place = getPlaceById(selected); place && place.deselect(); if (id !== null) { place = getPlaceById(id); if (place) { select(); } else { Ox.print() // async && place doesn't exist yet self.options.places({ keys: self.placeKeys, query: { conditions: [ {key: 'id', value: id, operator: '='} ] } }, function(results) { place = new Ox.MapPlace(Ox.extend({ map: that }, results.data.items[0])).add(); self.places.push(place); select(); }); } } else { place = null; select(); } } function select() { place && place.select(); self.options.selected = id; setStatus(); that.triggerEvent('selectplace', place); } }; function setScale() { var metersPerPixel = getMetersPerPixel(); Ox.forEach(self.scaleMeters, function(meters) { var scaleWidth = Math.round(meters / metersPerPixel); if (scaleWidth <= self.options.width / 2 - 4) { self.$scaleLabel .options({ title: '\u2190 ' + ( meters > 1000 ? Ox.formatNumber(meters / 1000) + ' k' : meters + ' ' ) + 'm \u2192' }) .css({ width: (scaleWidth - 16) + 'px' }) return false; } }); } function setStatus() { //Ox.print('setStatus()', self.options.selected) var code, country, disabled, place, title; if (self.options.statusbar) { place = getSelectedPlace(); country = place ? Ox.getCountryByGeoname(place.geoname) : ''; code = country ? country.code : 'NTHH'; disabled = place && !place.editable; if (place) { title = place.id[0] == '_' ? 'Add Place' : 'Remove Place'; } else { title = 'New Place'; } self.$placeFlag.attr({ src: Ox.PATH + 'Ox.Geo/png/icons/16/' + code + '.png' }); 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.showLabels = !self.options.showLabels //Ox.print('toggle', getMapType()) 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(); if (self.options.statusbar) { self.$placeNameInput.options({ width: Math.floor((width - 132) / 2) }); self.$placeGeonameInput.options({ width: Math.ceil((width - 132) / 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.addPlace = function(data) { addPlaceToPlaces(data); }; 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.newPlace = function() { addPlaceToMap(); }; that.panToPlace = function() { Ox.print('panToPlace:', self.options.selected) var place = getSelectedPlace(); place && self.map.panTo(place.center); return that; }; that.removePlace = function() { // fixme: removePlaceFromPlaces() ? removePlace(); 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.value = function(id, key, value) { // fixme: should be like the corresponding List/TextList/etc value function Ox.print('Map.value', id, key, value) getPlaceById(id)[key] = value; Ox.print('...', getPlaceById(id)) } that.zoomToPlace = function() { Ox.print('zoomToPlace') var place = getSelectedPlace(); place && self.map.fitBounds(place.bounds); return that; }; that.zoom = function(value) { zoom(value); return that; }; return that; };