// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ 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 find Initial query 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 zoombar If true, the map has a zoombar 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") selectplace Fires when a place has been selected or deselected place Place object # EXAMPLES ----------------------------------------------------------------- > Ox.Map() === true false > Ox.Map() === false false @*/ Ox.Map = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ // fixme: isClickable? hasZoombar? clickable: false, editable: false, find: '', findPlaceholder: 'Find', maxMarkers: 100, places: null, selected: '', showControls: false, showLabels: false, showTypes: false, statusbar: false, toolbar: false, zoombar: false // fixme: width, height }) .options(options || {}) .addClass('OxMap') .click(function(e) { !$(e.target).is('input') && that.gainFocus(); }) .bindEvent({ key_0: function() { panToPlace() }, key_c: toggleControls, 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() { 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 = Ox.Bar({ size: 24 }) .appendTo(that); self.$select = Ox.Select({ items: Ox.merge( self.options.editable ? [{id: 'new Place', title: 'New Place...', keyboard: 'n'}, {}] : [], [ { id: 'toggleLabels', title: self.options.showLabels ? ['Hide Labels', 'Show Labels'] : ['Show Labels', 'Hide Labels'], keyboard: 'l' }, { id: 'toggleControls', title: self.options.showControls ? ['Hide Controls', 'Show Controls'] : ['Show Controls', 'Hide Controls'], keyboard: 'c' } ] ), selectable: false, title: 'Options...', width: 96 }) .css({float: 'left', margin: '4px'}) .bindEvent({ click: function(data) { if (data.id == 'toggleLabels') { toggleLabels(); } else if (data.id == 'toggleControls') { toggleControls(); } } }) .appendTo(self.$toolbar); /* self.$labelsButton = Ox.Checkbox({ title: 'Labels', width: 64 }) .css({float: 'left', margin: '4px'}) .bindEvent({ change: toggleLabels }) .appendTo(self.$toolbar) */ self.$findInput = Ox.Input({ clear: true, placeholder: self.options.findPlaceholder, width: 192 }) .css({float: 'right', margin: '4px'}) .bindEvent({ submit: submitFind }) .appendTo(self.$toolbar) } self.$map = 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 = Ox.Bar({ size: 16 }) .css({ bottom: self.options.statusbar * 24 + 'px' }) .appendTo(that); } if (self.options.statusbar) { self.$statusbar = 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 = 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 = 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 = Ox.Button({ title: 'New Place', width: 96 }) .css({float: 'right', margin: '4px 4px 4px 2px'}) .bindEvent({ click: clickPlaceButton }) .appendTo(self.$statusbar); } self.$controls = { 'center': Ox.Button({ title: 'center', type: 'image' }) .addClass('OxMapControl OxMapButtonCenter') .bindEvent({ singleclick: function() { panToPlace(); }, doubleclick: function() { zoomToPlace(); } }), 'east': Ox.Button({ title: 'right', type: 'image' }) .addClass('OxMapControl OxMapButtonEast') .bindEvent({ singleclick: function() { pan(1, 0); }, doubleclick: function() { pan(2, 0); } }), 'north': Ox.Button({ title: 'up', type: 'image' }) .addClass('OxMapControl OxMapButtonNorth') .bindEvent({ singleclick: function() { pan(0, -1); }, doubleclick: function() { pan(0, -2); } }), 'south': Ox.Button({ title: 'down', type: 'image' }) .addClass('OxMapControl OxMapButtonSouth') .bindEvent({ singleclick: function() { pan(0, 1); }, doubleclick: function() { pan(0, 2); } }), 'west': Ox.Button({ title: 'left', type: 'image' }) .addClass('OxMapControl OxMapButtonWest') .bindEvent({ singleclick: function() { pan(-1, 0); }, doubleclick: function() { pan(-2, 0); } }), 'scale': Ox.Label({ textAlign: 'center', title: '...' // fixme ??? }) .addClass('OxMapControl') }; !self.options.showControls && Ox.forEach(self.$controls, function($control) { $control.css({opacity: 0}).hide(); }); 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, shift-click, or new place button Ox.print('addPlaceToMap', place) 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({ alternativeNames: [], countryCode: '', editable: true, geoname: '', id: '_' + Ox.encodeBase32(Ox.uid()), // fixme: stupid map: that, name: '', type: 'feature', 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(data) { self.map.setZoom(data.value); } function clickMap(event) { var place = getSelectedPlace(); if (self.options.clickable/* && !editing()*/) { getPlaceByLatLng(event.latLng, self.map.getBounds(), function(place) { if (place) { addPlaceToMap(place); //selectPlace(place.id); } else { selectPlace(null); } }); } else { pressEscape(); } } 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 = 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 crossesDateline() { var bounds = self.map.getBounds(); return bounds.getSouthWest().lng() > bounds.getNorthEast().lng(); } 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(); } self.maxZoomService.getMaxZoomAtLatLng(point, function(data) { callback(data.status == 'OK' ? data.zoom : null); }); } function getMetersPerPixel() { // m/px = m/deg * deg/px var degreesPerPixel = 360 / (Ox.MAP_TILE_SIZE * Math.pow(2, self.map.getZoom())); return Ox.getMetersPerDegree(self.map.getCenter().lat()) * degreesPerPixel; } function getMinZoom() { return self.mapHeight > 1024 ? 3 : self.mapHeight > 512 ? 2 : self.mapHeight > 256 ? 1 : 0; // fixme: there must be a function for this... /* 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()) 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 = self.minZoom; 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); if (self.options.find) { self.$findInput.options({value: self.options.find}) .triggerEvent('submit', {value: self.options.find}); } else if (self.options.selected) { selectPlace(self.options.selected, true); } else { if (mapBounds) { if (isEmpty(mapBounds)) { self.map.setZoom(self.minZoom); } else { self.map.fitBounds(mapBounds); } } if (self.map.getZoom() < self.minZoom) { self.map.setZoom(self.minZoom); } } updateFormElements(); // fixme: this is just guessing // setTimeout(updateFormElements, 2500); 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.$controls, function($control) { $control.appendTo(self.$map); }); } } function isEmpty(bounds) { // Google's bounds.isEmpty() is not reliable var southWest = bounds.getSouthWest(), northEast = bounds.getNorthEast(); return southWest.lat() == northEast.lat() && southWest.lng() == northEast.lng(); } 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(); 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 ('sG cD', spansGlobe(), crossesDateline()) self.options.places({ keys: self.placeKeys, query: { conditions: Ox.merge([ {key: 'lat', value: [south, north], operator: '-'} ], spansGlobe() ? [ {key: 'lng', value: [-180, 180], operator: '-'} ] : crossesDateline() ? [ {key: 'lng', value: [east, west], operator: '!-'} ] : [ {key: 'lng', value: [west, east], 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 panToPlace() { var place = getSelectedPlace(); place && self.map.panTo(place.center); } function parseGeodata(data) { var bounds = data.geometry.bounds || data.geometry.viewport, northEast = bounds.getNorthEast(), southWest = bounds.getSouthWest(), place = { alternativeNames: [], components: data.address_components, countryCode: getCountryCode(data.address_components), east: northEast.lng(), editable: self.options.editable, fullGeoname: getFullGeoname(data.address_components), id: '_' + Ox.encodeBase32(Ox.uid()), map: that, north: northEast.lat(), south: southWest.lat(), type: getType(data.address_components[0].types), west: southWest.lng() }; place.geoname = data.formatted_address || place.fullGeoname; place.name = (place.geoname || place.fullGeoname).split(', ')[0]; if (Math.abs(place.west) == 180 && Math.abs(place.east) == 180) { place.west = -179.99999999; place.east = 179.99999999; } place.south = Ox.limit(place.south, Ox.MIN_LATITUDE, Ox.MAX_LATITUDE - 0.00000001); place.north = Ox.limit(place.north, Ox.MIN_LATITUDE + 0.00000001, Ox.MAX_LATITUDE); 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', 'colloquial_area'], 'city': ['locality'], 'borough': ['neighborhood', 'postal_code', 'sublocality'], 'street': [ 'intersection', 'route', 'street_address', 'street_number' ], 'building': [ 'airport', 'establishment', 'floor', 'premise', 'room', 'subpremise' ] }, type = 'feature'; Ox.forEach(strings, function(values, key) { Ox.forEach(values, function(value) { if (find(value)) { type = key; return false; } }); return type == 'feature'; }); 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, zoom) { // 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 { // async && place doesn't exist yet self.options.places({ keys: self.placeKeys, query: { conditions: [ {key: 'id', value: id, operator: '=='} ], operator: '&' } }, function(result) { if (result.data.items.length) { place = new Ox.MapPlace(Ox.extend({ map: that }, result.data.items[0])).add(); self.places.push(place); select(); if (zoom) { zoomToPlace(); } else { panToPlace(); } } }); } } else { place = null; select(); } } function select() { place && place.select(); self.options.selected = id; setStatus(); that.triggerEvent('selectplace', place); // FIXME: all these events should rather pass {place: 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.$controls.scale .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 spansGlobe() { // fixme: or self.options.width ?? return self.$map.width() > Ox.MAP_TILE_SIZE * Math.pow(2, self.map.getZoom()); }; function submitFind(data) { self.options.find = data.value; if (data.value === '') { if (self.options.selected[0] == '_') { selectPlace(null); } } else { that.findPlace(data.value, function(place) { setStatus(place); }); } } function toggleControls() { // fixme: that.find() doesn't work here var $controls = that.$element.find('.OxMapControl'); self.options.showControls = !self.options.showControls; if (self.options.showControls) { $controls.show().animate({opacity: 1}, 250); } else { $controls.animate({opacity: 0}, 250, function() { $controls.hide(); }); } that.triggerEvent('togglecontrols', { visible: self.options.showControls }); } function toggleLabels() { self.options.showLabels = !self.options.showLabels; self.map.setMapTypeId(google.maps.MapTypeId[getMapType()]); that.triggerEvent('togglelabels', { visible: self.options.showLabels }); } 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(); if (self.options.zoombar) { getMaxZoom(function(zoom) { self.maxZoom = zoom; 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() { var place = getSelectedPlace(); place && self.map.fitBounds(place.bounds); } self.setOption = function(key, value) { if (key == 'height' || key == 'width') { that.resizeMap(); } else if (key == 'places') { //fixme: should zoom to new bounds zoom(0); } 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.getSelectedPlace = function() { return getSelectedPlace(); } that.editPlace = function() { getSelectedPlace().edit(); return that; }; /* that.editPlace = function(data) { var place = getPlaceById(self.options.selected); place.$marker.options(data); return that; }; */ that.findPlace = function(name, callback) { getPlaceByName(name, function(place) { if (place) { addPlaceToMap(place); self.map.fitBounds(place.bounds); } else { name && self.$findInput.addClass('OxError'); } callback(place); }); }; that.newPlace = function(place) { addPlaceToMap(place); }; that.panToPlace = function() { panToPlace(); 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 }); */ self.options.height = that.$element.height(); self.options.width = that.$element.width(); self.mapHeight = getMapHeight(); self.minZoom = getMinZoom(); if (self.minZoom > self.map.getZoom()) { self.map.setZoom(self.minZoom); } self.$map.css({ height: self.mapHeight + 'px', width: self.options.width + 'px' }); 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).options(key, value); } that.zoomToPlace = function() { zoomToPlace(); return that; }; that.zoom = function(value) { zoom(value); return that; }; return that; };