// vim: et:ts=4:sw=4:sts=4:ft=js Ox.Map = function(options, self) { self = self || {}; var that = new Ox.Element({}, self) .defaults({ // fixme: isClickable? hasZoombar? clickable: false, editable: false, findPlaceholder: 'Find', labels: false, markers: 100, places: [], selected: null, statusbar: false, toolbar: false, zoombar: false }) .options(options || {}) .addClass('OxMap') .click(function(e) { !$(e.target).is('input') && that.gainFocus(); }) .bindEvent({ key_0: function() { that.panToPlace() }, key_down: function() { pan(0, 1); }, key_enter: pressEnter, key_escape: pressEscape, key_equal: function() { zoom(1); }, key_l: toggleLabels, key_left: function() { pan(-1, 0); }, key_meta: function() { self.metaKey = true; $(document).one({ keyup: function() { self.metaKey = false; } }); }, key_minus: function() { zoom(-1); }, key_right: function() { pan(1, 0); }, key_shift: function() { self.shiftKey = true; $(document).one({ keyup: function() { self.shiftKey = false; } }); }, key_shift_down: function() { pan(0, 2); }, key_shift_0: function() { that.zoomToPlace(); }, key_shift_equal: function() { zoom(2) }, key_shift_left: function() { pan(-2, 0); }, key_shift_minus: function() { zoom(-2); }, key_shift_right: function() { pan(2, 0); }, key_shift_up: function() { pan(0, -2); }, key_up: function() { pan(0, -1); }, key_z: undo }); self.mapHeight = getMapHeight(); self.minZoom = getMinZoom(); self.scaleMeters = [ 50000000, 20000000, 10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000, 50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10 ]; Ox.extend(self, { metaKey: false, resultPlace: null, shiftKey: false }); if (self.options.toolbar) { self.$toolbar = new Ox.Bar({ size: 24 }) .appendTo(that); self.$labelsButton = new Ox.Button({ title: 'Show Labels', width: 96 }) .css({float: 'left', margin: '4px'}) .bindEvent({ click: toggleLabels }) .appendTo(self.$toolbar) self.$findInput = new Ox.Input({ clear: true, placeholder: self.options.findPlaceholder, width: 192 }) .css({float: 'right', margin: '4px'}) .bindEvent({ submit: submitFind }) .appendTo(self.$toolbar) } self.$map = new Ox.Element() .css({ left: 0, top: self.options.toolbar * 24 + 'px', right: 0, bottom: self.options.zoombar * 16 + self.options.statusbar * 24 + 'px' }) .appendTo(that); if (self.options.zoombar) { self.$zoombar = new Ox.Bar({ size: 16 }) .css({ bottom: self.options.statusbar * 24 + 'px' }) .appendTo(that); } if (self.options.statusbar) { self.$statusbar = new Ox.Bar({ size: 24 }) .css({ bottom: 0 }) .appendTo(that); self.$placeNameInput = new Ox.Input({ placeholder: 'Name', width: 96 }) //.css({position: 'absolute', left: 4, top: 4}) .css({float: 'left', margin: '4px 1px 4px 4px'}) .appendTo(self.$statusbar); self.$placeGeonameInput = new Ox.Input({ placeholder: 'Geoname', width: 96 }) //.css({position: 'absolute', left: 104, top: 4}) .css({float: 'left', margin: '4px 1px 4px 4px'}) .appendTo(self.$statusbar); self.$placeButton = new Ox.Button({ title: 'New Place', width: 96 }) .css({float: 'right', margin: '4px 4px 4px 2px'}) .bindEvent({ click: clickPlaceButton }) .appendTo(self.$statusbar); } self.$navigationButtons = { 'center': new Ox.Button({ title: 'close', type: 'image' }) .addClass('OxMapButton') .css({ left: '24px', top: '24px' }), 'east': new Ox.Button({ title: 'right', type: 'image' }) .addClass('OxMapButton') .css({ left: '44px', top: '24px', }), 'north': new Ox.Button({ title: 'up', type: 'image' }) .addClass('OxMapButton') .css({ left: '24px', top: '4px', }), 'south': new Ox.Button({ title: 'down', type: 'image' }) .addClass('OxMapButton') .css({ left: '24px', top: '44px', }), 'west': new Ox.Button({ title: 'left', type: 'image' }) .addClass('OxMapButton') .css({ left: '4px', top: '24px', }) }; self.$scaleLabel = new Ox.Label({ textAlign: 'center', title: '...' }) .addClass('OxMapLabel') .css({ right: '4px', top: '4px' }); if (!window.googleCallback) { window.googleCallback = function() { delete window.googleCallback; initMap(); }; $.getScript('http://maps.google.com/maps/api/js?callback=googleCallback&sensor=false'); } else { (function interval() { window.google ? initMap() : setTimeout(interval, 100); }()); } function addPlaceToMap(place) { // via find, click, or new place button var exists = false; if (!place) { var bounds = self.map.getBounds(), center = self.map.getCenter(), southwest = new google.maps.LatLngBounds( bounds.getSouthWest(), center ).getCenter(), northeast = new google.maps.LatLngBounds( center, bounds.getNorthEast() ).getCenter(), place = new Ox.MapPlace({ countryCode: '', editable: true, geoname: '', id: '_' + Ox.uid(), // fixme: stupid lat: center.lat(), lng: center.lng(), map: that, name: '', south: southwest.lat(), west: southwest.lng(), north: northeast.lat(), east: northeast.lng() }); } Ox.forEach(self.places, function(p, i) { if (place.bounds.equals(p.bounds)) { place = p; exists = true; return false; } }); if (!exists) { self.resultPlace && self.resultPlace.remove(); self.resultPlace = place; place.add(); } selectPlace(place.id); } function addPlaceToPlaces() { var place = getSelectedPlace(); if (self.options.selected == place.id) { self.options.selected = place.id.substr(1); } place.id = place.id.substr(1); // fixme: NOT SAFE! place.name = self.$placeNameInput.value(); place.geoname = self.$placeGeonameInput.value(); place.countryCode = Ox.getCountryCode(place.geoname); place.marker.update(); self.places.push(place); self.resultPlace = null; that.triggerEvent('addplace', place) Ox.print('SSSS', self.options.selected) } function boundsChanged() { setScale(); self.boundsChanged = true; } function canContain(outerBounds, innerBounds) { // checks if outerBounds _can_ contain innerBounds var outerSpan = outerBounds.toSpan(), innerSpan = innerBounds.toSpan(); return outerSpan.lat() > innerSpan.lat() && outerSpan.lng() > innerSpan.lng(); } function centerChanged() { self.center = self.map.getCenter(); self.centerChanged = true; } function changeZoom(event, data) { self.map.setZoom(data.value); } function clickMap(event) { Ox.print('Ox.Map clickMap') if (self.options.clickable/* && !editing()*/) { getPlaceByLatLng(event.latLng, self.map.getBounds(), function(place) { Ox.print('>>>>', place) if (place) { addPlaceToMap(place); //selectPlace(place.id); } else { selectPlace(null); } }); } } function clickPlaceButton() { var place = getSelectedPlace(), title = self.$placeButton.options('title'); if (title == 'New Place') { addPlaceToMap(); } else if (title == 'Add Place') { addPlaceToPlaces(); } else if (title == 'Remove Place') { } } function constructZoomInput() { Ox.print('constructZoomInput', self.minZoom, self.maxZoom) if (self.options.zoombar) { self.$zoomInput && self.$zoomInput.removeElement(); self.$zoomInput = new Ox.Range({ arrows: true, max: self.maxZoom, min: self.minZoom, size: that.width(), thumbSize: 32, thumbValue: true, value: self.map.getZoom() }) .bindEvent({ change: changeZoom }) .appendTo(self.$zoombar); } } function editing() { var place = getSelectedPlace(); return place && place.editing; } function getElevation(point, callback) { // fixme: unused if (arguments.length == 1) { callback = point; point = self.map.getCenter(); } self.elevationService.getElevationForLocations({ locations: [point] }, function(data) { callback(data.elevation); }); } function getMapHeight() { return self.options.height - self.options.statusbar * 24 - self.options.toolbar * 24 - self.options.zoombar * 16; } function getMapType() { return self.options.labels ? 'HYBRID' : 'SATELLITE' } function getMaxZoom(point, callback) { if (arguments.length == 1) { callback = point; point = self.map.getCenter(); } self.maxZoomService.getMaxZoomAtLatLng(point, function(data) { callback(data.status == 'OK' ? data.zoom : null); }); } function getMinZoom() { return 0; return Math.ceil( Ox.log(self.mapHeight / Ox.MAP_TILE_SIZE, 2) ); } function getPlaceById(id) { var place = Ox.getObjectById(self.places, id); if (!place && self.resultPlace && self.resultPlace.id == id) { place = self.resultPlace; } Ox.print('getPlaceById', id, place) return place; } function getPlaceByLatLng(latlng, bounds, callback) { // gets the largest place at latlng that would fit in bounds Ox.print('ll b', latlng, bounds) var callback = arguments.length == 3 ? callback : bounds, bounds = arguments.length == 3 ? bounds : null; self.geocoder.geocode({ latLng: latlng }, function(results, status) { Ox.print('results', results) var length = results.length; if (status == google.maps.GeocoderStatus.OK) { if (bounds) { Ox.forEach(results.reverse(), function(result, i) { if ( i == length - 1 || canContain(bounds, result.geometry.bounds || result.geometry.viewport) ) { callback(new Ox.MapPlace(parseGeodata(results[i]))); return false; } }); } else { callback(new Ox.MapPlace(parseGeodata(results[0]))); } } if ( status == google.maps.GeocoderStatus.OK || status == google.maps.GeocoderStatus.ZERO_RESULTS ) { triggerGeocodeEvent({ latLng: latlng, results: results }); } else { Ox.print('geocode failed:', status); callback(null); } }); } function getPlaceByName(name, callback) { self.geocoder.geocode({ address: name }, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { callback(new Ox.MapPlace(parseGeodata(results[0]))); } if ( status == google.maps.GeocoderStatus.OK || status == google.maps.GeocoderStatus.ZERO_RESULTS ) { triggerGeocodeEvent({ address: name, results: results }); } else { Ox.print('geocode failed:', status); callback(null); } }); } function getPositionByName(name) { var position = -1; Ox.forEach(self.options.places, function(place, i) { if (place.name == name) { position = i; return false; } }); return position; } function getSelectedMarker() { // needed in case self.options.selected // is changed from outside var id = null; if (self.resultPlace && self.resultPlace.selected) { id = self.resultPlace.id; } else { Ox.forEach(self.places, function(place) { if (place.selected) { id = place.id; return false; } }); } return id; } function getSelectedPlace() { return self.options.selected ? getPlaceById(self.options.selected) : null; } function initMap() { var mapBounds; updateFormElements(); self.elevationService = new google.maps.ElevationService(); self.geocoder = new google.maps.Geocoder(); self.maxZoomService = new google.maps.MaxZoomService(); self.places = []; self.options.places.forEach(function(place, i) { var bounds = new google.maps.LatLngBounds( new google.maps.LatLng(place.south, place.west), new google.maps.LatLng(place.north, place.east) ); if (Ox.isUndefined(place.id)) { place.id = Ox.uid(); } mapBounds = i == 0 ? bounds : mapBounds.union(bounds); }); self.center = mapBounds ? mapBounds.getCenter() : new google.maps.LatLng(0, 0); self.zoom = 1; // fixme: should depend on height that.map = self.map = new google.maps.Map(self.$map.$element[0], { center: self.center, disableDefaultUI: true, disableDoubleClickZoom: true, mapTypeId: google.maps.MapTypeId[getMapType()], zoom: self.zoom }); google.maps.event.addListener(self.map, 'bounds_changed', boundsChanged); google.maps.event.addListener(self.map, 'center_changed', centerChanged); google.maps.event.addListener(self.map, 'click', clickMap); google.maps.event.addListener(self.map, 'idle', mapChanged); google.maps.event.addListener(self.map, 'zoom_changed', zoomChanged); google.maps.event.addListenerOnce(self.map, 'tilesloaded', tilesLoaded); mapBounds && self.map.fitBounds(mapBounds); /* setTimeout(function() { }, 1000); */ self.options.places.forEach(function(place, i) { self.places[i] = new Ox.MapPlace(Ox.extend({ map: that }, place))/*.add()*/; }); google.maps.event.trigger(self.map, 'resize'); //that.gainFocus(); that.triggerEvent('load'); function tilesLoaded() { // fixme: can add earlier, use don't replace map contents option Ox.forEach(self.$navigationButtons, function(button) { button.appendTo(self.$map); }); self.$scaleLabel.appendTo(self.$map); } } function mapChanged() { // gets called after panning or zooming Ox.print('mapChanged'); var bounds; if (self.boundsChanged) { bounds = self.map.getBounds(); self.places.sort(function(a, b) { var sort = { a: a.selected ? Infinity : bounds.contains(a.center) ? a.size : -Infinity, b: b.selected ? Infinity : bounds.contains(b.center) ? b.size : -Infinity, }; return sort.b - sort.a; }).forEach(function(place, i) { if (i < self.options.markers && !place.visible) { place.add(); } else if (i >= self.options.markers && place.visible) { place.remove(); } }); self.boundsChanged = false; } if (self.centerChanged) { getMaxZoom(function(zoom) { if (zoom != self.maxZoom) { self.maxZoom = zoom; if (self.map.getZoom() > zoom) { self.map.setZoom(zoom); } constructZoomInput(); } }); self.centerChanged = false; } if (self.zoomChanged) { self.zoomChanged = false; } } function pan(x, y) { self.map.panBy(x * self.$map.width() / 2, y * self.$map.height() / 2); }; function parseGeodata(data) { var bounds = data.geometry.bounds || data.geometry.viewport, place = { components: data.address_components, countryCode: getCountryCode(data.address_components), east: bounds.getNorthEast().lng(), editable: self.options.editable, fullGeoname: getFullGeoname(data.address_components), geoname: data.formatted_address, id: '_' + Ox.uid(), map: that, name: data.formatted_address.split(', ')[0], north: bounds.getNorthEast().lat(), south: bounds.getSouthWest().lat(), types: data.types.map(function(type) { return Ox.toTitleCase(type.replace(/_/g, ' ')); }), west: bounds.getSouthWest().lng() }; function getCountryCode(components) { countryCode = ''; Ox.forEach(components, function(component) { if (component.types.indexOf('country') > -1) { countryCode = component.short_name; return false; } }); return countryCode; } function getFullGeoname(components) { var country = false; return components.map(function(component, i) { var name = component.long_name; if (i && components[i - 1].types.indexOf('country') > -1) { country = true; } return !country && ( i == 0 || name != components[i - 1].long_name ) ? name : null; }).join(', ') } return place; } function pressEnter() { var place = getSelectedPlace(); if (place) { if (place.editing) { place.submit(); } else { place.edit(); } } else if (self.resultPlace) { selectPlace(self.resultPlace.id) } } function pressEscape() { var place = getSelectedPlace(); if (place) { if (place.editing) { place.cancel(); } else { selectPlace(null); } } else if (self.resultPlace) { self.resultPlace.remove(); self.resultPlace = null; } } function removePlace(id) { } function reset() { //Ox.print(self.map.getZoom(), self.zoom); self.map.getZoom() == self.zoom ? self.map.panTo(self.center) : self.map.fitBounds(self.bounds); } function resizeMap() { /* Ox.print('resizeMap', self.options.width, self.options.height); var center = self.map.getCenter(); self.mapHeight = getMapHeight(); self.minZoom = getMinZoom(); that.css({ height: self.options.height + 'px', width: self.options.width + 'px' }); self.$map.css({ height: self.mapHeight + 'px', width: self.options.width + 'px' }); google.maps.event.trigger(self.map, 'resize'); self.map.setCenter(center); */ } function selectPlace(id) { var place, selected = getSelectedMarker(); Ox.print('Ox.Map selectPlace()', id, selected); if (id != selected) { place = getPlaceById(selected); place && place.deselect(); place = getPlaceById(id); place && place.select(); self.options.selected = id; setStatus(); that.triggerEvent('selectplace', place); } }; function getMetersPerPixel() { var mapWidth = self.$map.width(), span = self.map.getBounds().toSpan().lng(); if (span >= 360) { span = 360 * mapWidth / Ox.MAP_TILE_SIZE; } return span * Ox.getMetersPerDegree(self.map.getCenter().lat()) / mapWidth; } function setScale() { var metersPerPixel = getMetersPerPixel(); Ox.forEach(self.scaleMeters, function(meters) { var scaleWidth = Math.round(meters / metersPerPixel); if (scaleWidth <= 256) { self.$scaleLabel .options({ title: '\u2190 ' + ( meters > 1000 ? Ox.formatNumber(meters / 1000) + ' k' : meters + ' ' ) + 'm \u2192' }) .css({ width: (scaleWidth - 10) + 'px' }) return false; } }); } function setStatus() { Ox.print('setStatus()', self.options.selected) var disabled, place, title; if (self.options.statusbar) { place = getSelectedPlace(); if (place) { title = place.id[0] == '_' ? 'Add Place' : 'Remove Place'; } else { title = 'New Place'; } disabled = place && !place.editable; self.$placeNameInput.options({ disabled: disabled, value: place ? place.name : '' }); self.$placeGeonameInput.options({ disabled: disabled, value: place ? place.geoname : '' }); self.$placeButton.options({ disabled: disabled, title: title }); } Ox.print('STATUS DONE'); } function submitFind(event, data) { that.findPlace(data.value, function(place) { setStatus(place); }); } function toggleLabels() { self.options.labels = !self.options.labels self.map.setMapTypeId(google.maps.MapTypeId[getMapType()]); self.$labelsButton.options({ title: self.$labelsButton.options('title') == 'Show Labels' ? 'Hide Labels' : 'Show Labels' }); } function triggerGeocodeEvent(data) { // someone may want to cache google geocode data, so we fire an event. // google puts functions like lat or lng on the objects' prototypes, // so we create properly named properties, for json encoding if (data.latLng) { data.latLng = { lat: data.latLng.lat(), lng: data.latLng.lng() } } data.results.forEach(function(result) { ['bounds', 'viewport'].forEach(function(key) { if (result.geometry[key]) { result.geometry[key] = { northEast: { lat: result.geometry[key].getNorthEast().lat(), lng: result.geometry[key].getNorthEast().lng() }, southWest: { lat: result.geometry[key].getSouthWest().lat(), lng: result.geometry[key].getSouthWest().lng() } } } }); if (result.geometry.location) { result.geometry.location = { lat: result.geometry.location.lat(), lng: result.geometry.location.lng() } } }); that.triggerEvent('geocode', data); } function undo() { Ox.print('Map undo') var place = getSelectedPlace(); place.editing && place.undo(); } function updateFormElements() { var width = that.width(); self.$zoomInput && constructZoomInput(); self.$placeNameInput.options({ width: Math.floor((width - 112) / 2) }); self.$placeGeonameInput.options({ width: Math.ceil((width - 112) / 2) }); } function zoom(z) { self.map.setZoom(self.map.getZoom() + z); } function zoomChanged() { var zoom = self.map.getZoom(); if (zoom < self.minZoom) { self.map.setZoom(self.minZoom); } else if (self.maxZoom && zoom > self.maxZoom) { self.map.setZoom(self.maxZoom); } else { self.zoomChanged = true; self.$zoomInput && self.$zoomInput.options({value: zoom}); that.triggerEvent('zoom', { value: zoom }); } } function zoomToPlace() { Ox.print('zoomToPlace') if (self.options.selected !== null) { self.map.fitBounds(getPlaceById(self.options.selected).bounds); } } self.setOption = function(key, value) { /*if (key == 'height' || key == 'width') { resizeMap(); } else */if (key == 'places') { loadPlaces(); } else if (key == 'selected') { selectPlace(value); } else if (key == 'type') { } }; that.getKey = function() { var key = null; if (self.shiftKey) { key = 'shift' } else if (self.metaKey) { key = 'meta' } return key; } that.editPlace = function() { getPlaceById(self.options.selected).edit(); return that; } that.findPlace = function(name, callback) { getPlaceByName(name, function(place) { if (place) { addPlaceToMap(place); self.map.fitBounds(place.bounds); } else { self.$findInput.addClass('OxError'); } callback(place); }); }; that.panToPlace = function() { Ox.print('panToPlace:', self.options.selected) var place = getSelectedPlace(); place && self.map.panTo(place.center); return that; }; that.removePlace = function(id) { return that; }; that.resizeMap = function() { /* Ox.print('resizeMap', self.options.width, self.options.height); var center = self.map.getCenter(); self.mapHeight = getMapHeight(); self.minZoom = getMinZoom(); that.css({ height: self.options.height + 'px', width: self.options.width + 'px' }); self.$map.css({ height: self.mapHeight + 'px', width: self.options.width + 'px' }); google.maps.event.trigger(self.map, 'resize'); self.map.setCenter(center); */ /* Ox.print('Ox.Map.resizeMap()'); var center = self.map.getCenter(); self.options.height = that.$element.height(); self.options.width = that.$element.width(); Ox.print(self.options.width, self.options.height) self.$map.css({ height: self.mapHeight + 'px', width: self.options.width + 'px' }); google.maps.event.trigger(self.map, 'resize'); self.map.setCenter(center); self.options.zoombar && self.$zoomInput.options({ size: self.options.width }); */ updateFormElements(); google.maps.event.trigger(self.map, 'resize'); return that; } that.zoomToPlace = function() { Ox.print('zoomToPlace') var place = getSelectedPlace(); place && self.map.fitBounds(place.bounds); return that; }; that.zoom = function(value) { self.map.setZoom(value); return that; }; return that; };