// vim: et:ts=4:sw=4:sts=4:ft=javascript 'use strict'; /*@ 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 zoomOnlyWhenFocused If true, scroll-zoom only when focused 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, // FIXME: showStatusbar toolbar: false, // FIXME: showToolbar zoombar: false, // FIXME: showZoombar zoomOnlyWhenFocused: false // fixme: width, height }) .options(options || {}) .addClass('OxMap') .bindEvent({ gainfocus: function() { self.options.zoomOnlyWhenFocused && self.map.setOptions({scrollwheel: true}); }, losefocus: function() { self.options.zoomOnlyWhenFocused && self.map.setOptions({scrollwheel: false}); }, 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, mousedown: function(e) { !$(e.target).is('input') && that.gainFocus(); } }); self.isAsync = Ox.isFunction(self.options.places); self.mapHeight = getMapHeight(); self.metaKey = false; self.minZoom = getMinZoom(); self.placeKeys = [ 'id', 'name', 'alternativeNames', 'geoname', 'countryCode', 'type', 'lat', 'lng', 'south', 'west', 'north', 'east', 'area', 'editable' ]; self.resultPlace = null; self.scaleMeters = [ 50000000, 20000000, 10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000, 50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10 ]; self.shiftKey = false; if (self.options.toolbar) { self.$toolbar = Ox.Bar({ size: 24 }) .appendTo(that); self.$select = Ox.MenuButton({ 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' } ] ), 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' }) .addClass('OxMapControl OxMapScale') }; !self.options.showControls && Ox.forEach(self.$controls, function($control) { $control.css({opacity: 0}).hide(); }); self.$placeControls = { flag: Ox.Element() .addClass('OxPlaceControl OxPlaceFlag'), name: Ox.Label({ textAlign: 'center', tooltip: 'Click to pan, doubleclick to zoom' }) .addClass('OxPlaceControl OxPlaceName') .bindEvent({ singleclick: function() { panToPlace(); }, doubleclick: function() { zoomToPlace(); } }), deselectButton: Ox.Button({ title: 'close', tooltip: 'Deselect', type: 'image', }) .addClass('OxPlaceControl OxPlaceDeselectButton') .bindEvent({ click: function() { selectPlace(null); } }) } Ox.forEach(self.$placeControls, function($placeControl) { $placeControl.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.google) { initMap(); } else if (window.googleCallback) { (function interval() { window.google ? initMap() : setTimeout(interval, 100); }()); } else { window.googleCallback = function() { delete window.googleCallback; initMap(); }; $.getScript( document.location.protocol + '//maps.google.com/maps/api/js?callback=googleCallback&sensor=false' ); } function addPlaces() { Ox.forEach(self.$placeControls, function($placeControl) { $placeControl.hide(); }); self.places && self.places.forEach(function(place) { place.remove(); }); self.places = []; if (!self.isAsync) { self.options.places.forEach(function(place) { self.places.push(new Ox.MapPlace(Ox.extend({ map: that }, place))); }); } } function addPlaceToMap(place) { // via find, click, shift-click, or new place button Ox.Log('Map', '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); Ox.print('addPlaceToPlaces, place name:', place.name) self.options.selected = place.id; place.countryCode = country ? country.code : ''; Ox.Log('Map', 'addP2P', data, place); place.marker.update(); self.places.push(place); self.resultPlace = null; that.triggerEvent('addplace', place) } 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() { var tooltip = $('.OxMapMarkerTooltip'); tooltip.length && Ox.UI.elements[$(tooltip[0]).data('oxid')].hide(); 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(); } } function constructZoomInput() { if (self.options.zoombar) { self.$zoomInput && self.$zoomInput.remove(); self.$zoomInput = Ox.Range({ arrows: true, changeOnDrag: 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) { return self.resultPlace && self.resultPlace.id == id ? self.resultPlace : Ox.getObjectById(self.places, id); } function getPlaceByLatLng(latlng, bounds, callback) { // gets the largest place at latlng that would fit in bounds var callback = arguments.length == 3 ? callback : bounds, bounds = arguments.length == 3 ? bounds : null; self.geocoder.geocode({ latLng: latlng }, function(results, status) { //Ox.Log('Map', '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.Log('Map', '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.Log('Map', '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.Log('Map', '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()], noClear: true, scrollwheel: !self.options.zoomOnlyWhenFocused, 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); google.maps.event.trigger(self.map, 'resize'); // needed to get mouse x/y coordinates on marker mouseover, // see http://code.google.com/p/gmaps-api-issues/issues/detail?id=2342 that.overlayView = new google.maps.OverlayView(); that.overlayView.setMap(self.map); that.overlayView.draw = function () { if (!this.ready) { this.ready = true; google.maps.event.trigger(this, 'ready'); } } that.overlayView.draw(); addPlaces(); Ox.forEach(self.$controls, function($control) { $control.appendTo(self.$map); }); Ox.forEach(self.$placeControls, function($placeControl) { $placeControl.appendTo(self.$map); }); if (self.options.find) { self.$findInput .value(self.options.find) .triggerEvent('submit', {value: self.options.find}); } else { if (self.options.selected) { selectPlace(self.options.selected, true); } 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(); //that.gainFocus(); self.loaded = true; that.triggerEvent('load'); function tilesLoaded() { setTimeout(formatTerms, 250); } }); } 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 formatTerms() { setTimeout(function() { var $element; try { $element = $(self.$map.find('a[href$="terms_maps.html"]').parent()).css({ color: 'rgb(192, 192, 192)', textShadow: '1px 1px 0 rgb(64, 64, 64)' }); ['moz', 'o', 'webkit'].forEach(function(browser) { $element.css({ backgroundImage: '-' + browser + '-linear-gradient(left, rgba(255, 255, 255, 0) 0px, rgba(255, 255, 255, 0.1) 50px)', }); }); $element.children().css({color: 'rgb(192, 192, 192)'}); } catch (e) {} }, 0); } function mapChanged() { // gets called after panning or zooming Ox.print('Map', 'mapChanged'); 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 { 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.Log('Map', '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; } formatTerms(); } 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) { var 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.Log('Map', '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'; function find(type) { var ret; Ox.forEach(types, function(v) { ret = Ox.startsWith(v, type); return !ret; }); return ret; } Ox.forEach(strings, function(values, key) { Ox.forEach(values, function(value) { if (find(value)) { type = key; return false; } }); return type == 'feature'; }); return type; } 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; } } // fixme: removePlacefromMap? function removePlace() { var place = getSelectedPlace(); place.id = '_' + place.id; self.options.selected = place.id; self.places.splice(Ox.getIndexById(self.places, place.id), 1); self.resultPlace && self.resultPlace.remove(); self.resultPlace = place; Ox.print('removed place, place is now', place); place.marker.update(); } function reset() { self.map.getZoom() == self.zoom ? self.map.panTo(self.center) : self.map.fitBounds(self.bounds); } function selectPlace(id, zoom) { Ox.print('SELECT PLACE', id) // id can be null (deselect) var place, selected = getSelectedMarker(); Ox.Log('Map', '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; setPlaceControls(place); setStatus(); that.triggerEvent('selectplace', place); // FIXME: all these events should rather pass {place: place} } }; function setPlaceControls(place) { var $placeControls = that.$element.find('.OxPlaceControl'), isVisible = self.$placeControls.name.is(':visible'); if (place) { self.$placeControls.flag.empty().append( $('').attr({ src: Ox.getFlagByGeoname(place.geoname, 16) }) ).show(); self.$placeControls.name.options({title: place.name}) !isVisible && $placeControls.show().animate({opacity: 1}, 250); } else { isVisible && $placeControls.animate({opacity: 0}, 250, function() { $placeControls.hide(); }); } } 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.Log('Map', '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.Log('Map', '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.Log('Map', '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.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 == 'find') { self.$findInput .value(self.options.find) .triggerEvent('submit', {value: self.options.find}); } else if (key == 'height' || key == 'width') { that.$element.css(key, value + 'px'); that.resizeMap(); } else if (key == 'places') { // fixme: assumes !self.isAsync Ox.print('MAP SET OPTIONS PLACES', value); addPlaces(); getMapBounds(function(mapBounds) { if (mapBounds) { self.map.fitBounds(mapBounds); } else { self.map.setZoom(self.minZoom); self.map.setCenter(new google.maps.LatLng(0, 0)); } // fixme: the following is just a guess self.boundsChanged = true; mapChanged(); }); if (self.options.selected) { if (getSelectedPlace()) { selectPlace(self.options.selected); } else { self.options.selected = ''; } } } else if (key == 'selected') { self.loaded && selectPlace(value || null); } else if (key == 'type') { } }; that.addPlace = function(data) { addPlaceToPlaces(data); }; that.getKey = function() { return self.shiftKey ? 'shift' : self.metaKey ? 'meta' : null; }; 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.Log('Map', '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.Log('Map', 'Ox.Map.resizeMap()'); var center = self.map.getCenter(); self.options.height = that.$element.height(); self.options.width = that.$element.width(); Ox.Log('Map', 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(); // check if map has initialized if (self.map) { 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(); Ox.print('triggering google maps resize event, height', self.options.height) 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', '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; };