From: John Dong Date: Thu, 16 Aug 2007 22:26:41 +0000 (-0400) Subject: Add Google Map of voters X-Git-Url: https://projects.mako.cc/source/selectricity-live/commitdiff_plain/fcc68b4dc198b7cb0cf93467d96038b0844675fe Add Google Map of voters * Add rails plugin for google maps * Add rails plugin for IP to location mapping --- diff --git a/app/controllers/quickvote_controller.rb b/app/controllers/quickvote_controller.rb index 4783eb2..e327ed6 100644 --- a/app/controllers/quickvote_controller.rb +++ b/app/controllers/quickvote_controller.rb @@ -137,7 +137,22 @@ class QuickvoteController < ApplicationController render :nothing => true end - + def mapvoters + @map = GMap.new("map_div_id") + @map.control_init(:large_map => true, :map_type => true) + center=nil + QuickVote.ident_to_quickvote(params[:id]).voters.each do |voter| + next unless voter.ipaddress + location = GeoKit::Geocoders::IpGeocoder.geocode(voter.ipaddress) + next unless location.lng and location.lat + unless center + center=[location.lat,location.lng] + @map.center_zoom_init(center,4) + end + marker = GMarker.new([location.lat,location.lng], :title => "Voter", :info_window => (voter.ipaddress or "unknown")+" "+voter.vote.votestring) + @map.overlay_init(marker) + end + end ############################################################### # the following method pertains to displaying the results of a # quickvote diff --git a/app/views/quickvote/mapvoters.rhtml b/app/views/quickvote/mapvoters.rhtml new file mode 100644 index 0000000..8f94cd0 --- /dev/null +++ b/app/views/quickvote/mapvoters.rhtml @@ -0,0 +1,3 @@ +<%= GMap.header %> +<%= @map.to_html %> +<%= @map.div(:width => 800, :height => 400) %> diff --git a/app/views/quickvote/results.rhtml b/app/views/quickvote/results.rhtml index 24a9beb..0cbd6d0 100644 --- a/app/views/quickvote/results.rhtml +++ b/app/views/quickvote/results.rhtml @@ -144,7 +144,7 @@ by several other names.

-

Voters

+

Voters <%= link_to "[Stalk Voters]", :controller => "quickvote", :action => "mapvoters", :id => @election.id %>

diff --git a/config/environment.rb b/config/environment.rb index 77d020c..ba92734 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -88,3 +88,53 @@ end ActionMailer::Base.delivery_method = :sendmail ActionMailer::Base.default_charset = "utf-8" +# These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable +GeoKit::default_units = :miles +GeoKit::default_formula = :sphere + +# This is the timeout value in seconds to be used for calls to the geocoder web +# services. For no timeout at all, comment out the setting. The timeout unit +# is in seconds. +GeoKit::Geocoders::timeout = 3 + +# These settings are used if web service calls must be routed through a proxy. +# These setting can be nil if not needed, otherwise, addr and port must be +# filled in at a minimum. If the proxy requires authentication, the username +# and password can be provided as well. +GeoKit::Geocoders::proxy_addr = nil +GeoKit::Geocoders::proxy_port = nil +GeoKit::Geocoders::proxy_user = nil +GeoKit::Geocoders::proxy_pass = nil + +# This is your yahoo application key for the Yahoo Geocoder. +# See http://developer.yahoo.com/faq/index.html#appid +# and http://developer.yahoo.com/maps/rest/V1/geocode.html +GeoKit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' + +# This is your Google Maps geocoder key. +# See http://www.google.com/apis/maps/signup.html +# and http://www.google.com/apis/maps/documentation/#Geocoding_Examples +GeoKit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' + +# This is your username and password for geocoder.us. +# To use the free service, the value can be set to nil or false. For +# usage tied to an account, the value should be set to username:password. +# See http://geocoder.us +# and http://geocoder.us/user/signup +GeoKit::Geocoders::geocoder_us = false + +# This is your authorization key for geocoder.ca. +# To use the free service, the value can be set to nil or false. For +# usage tied to an account, set the value to the key obtained from +# Geocoder.ca. +# See http://geocoder.ca +# and http://geocoder.ca/?register=1 +GeoKit::Geocoders::geocoder_ca = false + +# This is the order in which the geocoders are called in a failover scenario +# If you only want to use a single geocoder, put a single symbol in the array. +# Valid symbols are :google, :yahoo, :us, and :ca. +# Be aware that there are Terms of Use restrictions on how you can use the +# various geocoders. Make sure you read up on relevant Terms of Use for each +# geocoder you are going to use. +GeoKit::Geocoders::provider_order = [:google,:us] diff --git a/config/gmaps_api_key.yml b/config/gmaps_api_key.yml new file mode 100644 index 0000000..b1641f5 --- /dev/null +++ b/config/gmaps_api_key.yml @@ -0,0 +1,13 @@ +#Fill here the Google Maps API keys for your application +#In this sample: +#For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode. +#In production, the app can be accessed through 2 different hosts: thepochisuperstarmegashow.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test). + +development: + ABQIAAAA479zRK1hoNqMcKLTMuBcTRScPKE_l4RVNk_lv74wWGSk9YyVlRQ16fPZdTl-PBiKfGdEjSpSL8gVtA +test: + ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ + +production: + thepochisuperstarmegashow.com: ABQIAAAAzMUFFnT9uH0Sfg76Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDmlRT6e90j135zat56yhJKQlWnkaidDIQ + example.com: ABQIAAAAzMUFFnT9uH0Sfg98Y4kbhGFJQa0g3IQ9GZqIMmInSLrthJKGDmlRT98f4j135zat56yjRKQlWnkmod3TB diff --git a/public/javascripts/clusterer.js b/public/javascripts/clusterer.js new file mode 100644 index 0000000..eeb6dd9 --- /dev/null +++ b/public/javascripts/clusterer.js @@ -0,0 +1,444 @@ +// Clusterer.js - marker clustering routines for Google Maps apps +// +// The original version of this code is available at: +// http://www.acme.com/javascript/ +// +// Copyright © 2005,2006 by Jef Poskanzer . +// All rights reserved. +// +// Modified for inclusion into the YM4R library in accordance with the +// following license: +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// For commentary on this license please see http://www.acme.com/license.html + + +// Constructor. +Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) { + this.markers = []; + if(markers){ + for(var i =0 ; i< markers.length ; i++){ + this.addMarker(markers[i]); + } + } + this.clusters = []; + this.timeout = null; + + this.maxVisibleMarkers = maxVisibleMarkers || 150; + this.gridSize = gridSize || 5; + this.minMarkersPerCluster = minMarkersPerCluster || 5; + this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10; + + this.icon = icon || G_DEFAULT_ICON; +} + +Clusterer.prototype = new GOverlay(); + +Clusterer.prototype.initialize = function ( map ){ + this.map = map; + this.currentZoomLevel = map.getZoom(); + + GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) ); + //Set map for each marker + for(var i = 0,len = this.markers.length ; i < len ; i++){ + this.markers[i].setMap( map ); + } + this.displayLater(); +} + +Clusterer.prototype.remove = function(){ + for ( var i = 0; i < this.markers.length; ++i ){ + this.removeMarker(this.markers[i]); + } +} + +Clusterer.prototype.copy = function(){ + return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox); +} + +Clusterer.prototype.redraw = function(force){ + this.displayLater(); +} + +// Call this to change the cluster icon. +Clusterer.prototype.setIcon = function ( icon ){ + this.icon = icon; +} + +// Call this to add a marker. +Clusterer.prototype.addMarker = function ( marker, description){ + marker.onMap = false; + this.markers.push( marker ); + marker.description = marker.description || description; + if(this.map != null){ + marker.setMap(this.map); + this.displayLater(); + } +}; + + +// Call this to remove a marker. +Clusterer.prototype.removeMarker = function ( marker ){ + for ( var i = 0; i < this.markers.length; ++i ) + if ( this.markers[i] == marker ){ + if ( marker.onMap ) + this.map.removeOverlay( marker ); + for ( var j = 0; j < this.clusters.length; ++j ){ + var cluster = this.clusters[j]; + if ( cluster != null ){ + for ( var k = 0; k < cluster.markers.length; ++k ) + if ( cluster.markers[k] == marker ){ + cluster.markers[k] = null; + --cluster.markerCount; + break; + } + if ( cluster.markerCount == 0 ){ + this.clearCluster( cluster ); + this.clusters[j] = null; + } + else if ( cluster == this.poppedUpCluster ) + Clusterer.rePop( this ); + } + } + this.markers[i] = null; + break; + } + this.displayLater(); +}; + +Clusterer.prototype.displayLater = function (){ + if ( this.timeout != null ) + clearTimeout( this.timeout ); + this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 ); +}; + +Clusterer.display = function ( clusterer ){ + var i, j, marker, cluster, len, len2; + + clearTimeout( clusterer.timeout ); + + var newZoomLevel = clusterer.map.getZoom(); + if ( newZoomLevel != clusterer.currentZoomLevel ){ + // When the zoom level changes, we have to remove all the clusters. + for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){ + if ( clusterer.clusters[i] != null ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + } + clusterer.clusters.length = 0; + clusterer.currentZoomLevel = newZoomLevel; + } + + // Get the current bounds of the visible area. + var bounds = clusterer.map.getBounds(); + + // Expand the bounds a little, so things look smoother when scrolling + // by small amounts. + var sw = bounds.getSouthWest(); + var ne = bounds.getNorthEast(); + var dx = ne.lng() - sw.lng(); + var dy = ne.lat() - sw.lat(); + dx *= 0.10; + dy *= 0.10; + bounds = new GLatLngBounds( + new GLatLng( sw.lat() - dy, sw.lng() - dx ), + new GLatLng( ne.lat() + dy, ne.lng() + dx ) + ); + + // Partition the markers into visible and non-visible lists. + var visibleMarkers = []; + var nonvisibleMarkers = []; + for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){ + marker = clusterer.markers[i]; + if ( marker != null ) + if ( bounds.contains( marker.getPoint() ) ) + visibleMarkers.push( marker ); + else + nonvisibleMarkers.push( marker ); + } + + // Take down the non-visible markers. + for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){ + marker = nonvisibleMarkers[i]; + if ( marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + + // Take down the non-visible clusters. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){ + clusterer.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } + } + + // Clustering! This is some complicated stuff. We have three goals + // here. One, limit the number of markers & clusters displayed, so the + // maps code doesn't slow to a crawl. Two, when possible keep existing + // clusters instead of replacing them with new ones, so that the app pans + // better. And three, of course, be CPU and memory efficient. + if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){ + // Add to the list of clusters by splitting up the current bounds + // into a grid. + var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat(); + var latInc = latRange / clusterer.gridSize; + var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 ); + for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc ) + for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){ + cluster = new Object(); + cluster.clusterer = clusterer; + cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) ); + cluster.markers = []; + cluster.markerCount = 0; + cluster.onMap = false; + cluster.marker = null; + clusterer.clusters.push( cluster ); + } + + // Put all the unclustered visible markers into a cluster - the first + // one it fits in, which favors pre-existing clusters. + for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.inCluster ){ + for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){ + cluster = clusterer.clusters[j]; + if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){ + cluster.markers.push( marker ); + ++cluster.markerCount; + marker.inCluster = true; + } + } + } + } + + // Get rid of any clusters containing only a few markers. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ) + if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + + // Shrink the clusters list. + for ( i = clusterer.clusters.length - 1; i >= 0; --i ) + if ( clusterer.clusters[i] != null ) + break; + else + --clusterer.clusters.length; + + // Ok, we have our clusters. Go through the markers in each + // cluster and remove them from the map if they are currently up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null ){ + for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){ + marker = cluster.markers[j]; + if ( marker != null && marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + } + } + + // Now make cluster-markers for any clusters that need one. + for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && cluster.marker == null ){ + // Figure out the average coordinates of the markers in this + // cluster. + var xTotal = 0.0, yTotal = 0.0; + for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){ + marker = cluster.markers[j]; + if ( marker != null ){ + xTotal += ( + marker.getPoint().lng() ); + yTotal += ( + marker.getPoint().lat() ); + } + } + var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount ); + marker = new GMarker( location, { icon: clusterer.icon } ); + cluster.marker = marker; + GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) ); + } + } + } + + // Display the visible markers not already up and not in clusters. + for ( i = 0, len = visibleMarkers.length; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.onMap && ! marker.inCluster ) + { + clusterer.map.addOverlay( marker ); + marker.addedToMap(); + marker.onMap = true; + } + } + + // Display the visible clusters not already up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){ + clusterer.map.addOverlay( cluster.marker ); + cluster.onMap = true; + } + } + + // In case a cluster is currently popped-up, re-pop to get any new + // markers into the infobox. + Clusterer.rePop( clusterer ); +}; + + +Clusterer.popUp = function ( cluster ){ + var clusterer = cluster.clusterer; + var html = '
IP/Host
'; + var n = 0; + for ( var i = 0 , len = cluster.markers.length; i < len; ++i ) + { + var marker = cluster.markers[i]; + if ( marker != null ) + { + ++n; + html += ''; + if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox ) + { + html += ''; + break; + } + } + } + html += '
'; + if ( marker.getIcon().smallImage != null ) + html += ''; + else + html += ''; + html += '' + marker.description + '
...and ' + ( cluster.markerCount - n ) + ' more
'; + clusterer.map.closeInfoWindow(); + cluster.marker.openInfoWindowHtml( html ); + clusterer.poppedUpCluster = cluster; +}; + +Clusterer.rePop = function ( clusterer ){ + if ( clusterer.poppedUpCluster != null ) + Clusterer.popUp( clusterer.poppedUpCluster ); +}; + +Clusterer.popDown = function ( clusterer ){ + clusterer.poppedUpCluster = null; +}; + +Clusterer.prototype.clearCluster = function ( cluster ){ + var i, marker; + + for ( i = 0; i < cluster.markers.length; ++i ){ + if ( cluster.markers[i] != null ){ + cluster.markers[i].inCluster = false; + cluster.markers[i] = null; + } + } + + cluster.markers.length = 0; + cluster.markerCount = 0; + + if ( cluster == this.poppedUpCluster ) + this.map.closeInfoWindow(); + + if ( cluster.onMap ) + { + this.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } +}; + +// This returns a function closure that calls the given routine with the +// specified arg. +Clusterer.makeCaller = function ( func, arg ){ + return function () { func( arg ); }; +}; + + +// Augment GMarker so it handles markers that have been created but +// not yet addOverlayed. +GMarker.prototype.setMap = function ( map ){ + this.map = map; +}; + +GMarker.prototype.getMap = function (){ + return this.map; +} + +GMarker.prototype.addedToMap = function (){ + this.map = null; +}; + + +GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow; +GMarker.prototype.openInfoWindow = function ( node, opts ){ + if ( this.map != null ) + return this.map.openInfoWindow( this.getPoint(), node, opts ); + else + return this.origOpenInfoWindow( node, opts ); +}; + +GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml; +GMarker.prototype.openInfoWindowHtml = function ( html, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowHtml( this.getPoint(), html, opts ); + else + return this.origOpenInfoWindowHtml( html, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs; +GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts ); + else + return this.origOpenInfoWindowTabs( tabNodes, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml; +GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts ); + else + return this.origOpenInfoWindowTabsHtml( tabHtmls, opts ); +}; + +GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup; +GMarker.prototype.showMapBlowup = function ( opts ){ + if ( this.map != null ) + return this.map.showMapBlowup( this.getPoint(), opts ); + else + return this.origShowMapBlowup( opts ); +}; + + +function addDescriptionToMarker(marker, description){ + marker.description = description; + return marker; +} diff --git a/public/javascripts/geoRssOverlay.js b/public/javascripts/geoRssOverlay.js new file mode 100644 index 0000000..315c26d --- /dev/null +++ b/public/javascripts/geoRssOverlay.js @@ -0,0 +1,194 @@ +// GeoRssOverlay: GMaps API extension to display a group of markers from +// a RSS feed +// +// Copyright 2006 Mikel Maron (email: mikel_maron yahoo com) +// +// The original version of this code is called MGeoRSS and can be found +// at the following address: +// http://brainoff.com/gmaps/mgeorss.html +// +// Modified by Andrew Turner to add support for the GeoRss Simple vocabulary +// +// Modified and bundled with YM4R in accordance with the following +// license: +// +// This work is public domain + +function GeoRssOverlay(rssurl,icon,proxyurl,options){ + this.rssurl = rssurl; + this.icon = icon; + this.proxyurl = proxyurl; + if(options['visible'] == undefined) + this.visible = true; + else + this.visible = options['visible']; + this.listDiv = options['listDiv']; //ID of the item list DIV + this.contentDiv = options['contentDiv']; //ID of the content DIV + this.listItemClass = options['listItemClass']; //Class of the list item DIV + this.limitItems = options['limit']; //Maximum number of displayed entries + this.request = false; + this.markers = []; +} + +GeoRssOverlay.prototype = new GOverlay(); + +GeoRssOverlay.prototype.initialize=function(map) { + this.map = map; + this.load(); +} + +GeoRssOverlay.prototype.redraw = function(force){ + //nothing to do : the markers are already taken care of +} + +GeoRssOverlay.prototype.remove = function(){ + for(var i= 0, len = this.markers.length ; i< len; i++){ + this.map.removeOverlay(this.markers[i]); + } +} + +GeoRssOverlay.prototype.showHide=function() { + if (this.visible) { + for (var i=0;i" + title + "

" + description; + + if(this.contentDiv == undefined){ + GEvent.addListener(marker, "click", function() { + marker.openInfoWindowHtml(html); + }); + }else{ + var contentDiv = this.contentDiv; + GEvent.addListener(marker, "click", function() { + document.getElementById(contentDiv).innerHTML = html; + }); + } + + if(this.listDiv != undefined){ + var a = document.createElement('a'); + a.innerHTML = title; + a.setAttribute("href","#"); + var georss = this; + a.onclick = function(){ + georss.showMarker(index); + return false; + }; + var div = document.createElement('div'); + if(this.listItemClass != undefined){ + div.setAttribute("class",this.listItemClass); + } + div.appendChild(a); + document.getElementById(this.listDiv).appendChild(div); + } + + return marker; +} diff --git a/public/javascripts/markerGroup.js b/public/javascripts/markerGroup.js new file mode 100644 index 0000000..02fe624 --- /dev/null +++ b/public/javascripts/markerGroup.js @@ -0,0 +1,114 @@ +function GMarkerGroup(active, markers, markersById) { + this.active = active; + this.markers = markers || new Array(); + this.markersById = markersById || new Object(); +} + +GMarkerGroup.prototype = new GOverlay(); + +GMarkerGroup.prototype.initialize = function(map) { + this.map = map; + + if(this.active){ + for(var i = 0 , len = this.markers.length; i < len; i++) { + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } +} + +//If not already done (ie if not inactive) remove all the markers from the map +GMarkerGroup.prototype.remove = function() { + this.deactivate(); +} + +GMarkerGroup.prototype.redraw = function(force){ + //Nothing to do : markers are already taken care of +} + +//Copy the data to a new Marker Group +GMarkerGroup.prototype.copy = function() { + var overlay = new GMarkerGroup(this.active); + overlay.markers = this.markers; //Need to do deep copy + overlay.markersById = this.markersById; //Need to do deep copy + return overlay; +} + +//Inactivate the Marker group and clear the internal content +GMarkerGroup.prototype.clear = function(){ + //deactivate the map first (which removes the markers from the map) + this.deactivate(); + //Clear the internal content + this.markers = new Array(); + this.markersById = new Object(); +} + +//Add a marker to the GMarkerGroup ; Adds it now to the map if the GMarkerGroup is active +GMarkerGroup.prototype.addMarker = function(marker,id){ + if(id == undefined){ + this.markers.push(marker); + }else{ + this.markersById[id] = marker; + } + if(this.active && this.map != undefined ){ + this.map.addOverlay(marker); + } +} + +//Open the info window (or info window tabs) of a marker +GMarkerGroup.prototype.showMarker = function(id){ + var marker = this.markersById[id]; + if(marker != undefined){ + GEvent.trigger(marker,"click"); + } +} + +//Activate (or deactivate depending on the argument) the GMarkerGroup +GMarkerGroup.prototype.activate = function(active){ + active = (active == undefined) ? true : active; + if(!active){ + if(this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.removeOverlay(this.markers[i]) + } + for(var id in this.markersById){ + this.map.removeOverlay(this.markersById[id]); + } + } + this.active = false; + } + }else{ + if(!this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } + this.active = true; + } + } +} + +GMarkerGroup.prototype.centerAndZoomOnMarkers = function() { + if(this.map != undefined){ + //merge markers and markersById + var tmpMarkers = this.markers.slice(); + for (var id in this.markersById){ + tmpMarkers.push(this.markersById[id]); + } + if(tmpMarkers.length > 0){ + this.map.centerAndZoomOnMarkers(tmpMarkers); + } + } +} + +//Deactivate the Group Overlay (convenience method) +GMarkerGroup.prototype.deactivate = function(){ + this.activate(false); +} diff --git a/public/javascripts/wms-gs.js b/public/javascripts/wms-gs.js new file mode 100644 index 0000000..c67146b --- /dev/null +++ b/public/javascripts/wms-gs.js @@ -0,0 +1,69 @@ +/* + * Call generic wms service for GoogleMaps v2 + * John Deck, UC Berkeley + * Inspiration & Code from: + * Mike Williams http://www.econym.demon.co.uk/googlemaps2/ V2 Reference & custommap code + * Brian Flood http://www.spatialdatalogic.com/cs/blogs/brian_flood/archive/2005/07/11/39.aspx V1 WMS code + * Kyle Mulka http://blog.kylemulka.com/?p=287 V1 WMS code modifications + * http://search.cpan.org/src/RRWO/GPS-Lowrance-0.31/lib/Geo/Coordinates/MercatorMeters.pm + * + * Modified by Chris Holmes, TOPP to work by default with GeoServer. + * + * Bundled with YM4R with John Deck's permission. + * Slightly modified to fit YM4R. + * See johndeck.blogspot.com for the original version and for examples and instructions of how to use it. + */ + +var WGS84_SEMI_MAJOR_AXIS = 6378137.0; //equatorial radius +var WGS84_ECCENTRICITY = 0.0818191913108718138; +var DEG2RAD=0.0174532922519943; +var PI=3.14159267; + +function dd2MercMetersLng(p_lng) { + return WGS84_SEMI_MAJOR_AXIS * (p_lng*DEG2RAD); +} + +function dd2MercMetersLat(p_lat) { + var lat_rad = p_lat * DEG2RAD; + return WGS84_SEMI_MAJOR_AXIS * Math.log(Math.tan((lat_rad + PI / 2) / 2) * Math.pow( ((1 - WGS84_ECCENTRICITY * Math.sin(lat_rad)) / (1 + WGS84_ECCENTRICITY * Math.sin(lat_rad))), (WGS84_ECCENTRICITY/2))); +} + +function addWMSPropertiesToLayer(tile_layer,base_url,layers,styles,format,merc_proj,use_geo){ + tile_layer.format = format; + tile_layer.baseURL = base_url; + tile_layer.styles = styles; + tile_layer.layers = layers; + tile_layer.mercatorEpsg = merc_proj; + tile_layer.useGeographic = use_geo; + return tile_layer; +} + +getTileUrlForWMS=function(a,b,c) { + var lULP = new GPoint(a.x*256,(a.y+1)*256); + var lLRP = new GPoint((a.x+1)*256,a.y*256); + var lUL = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lULP,b,c); + var lLR = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lLRP,b,c); + + if (this.useGeographic){ + var lBbox=lUL.x+","+lUL.y+","+lLR.x+","+lLR.y; + var lSRS="EPSG:4326"; + }else{ + var lBbox=dd2MercMetersLng(lUL.x)+","+dd2MercMetersLat(lUL.y)+","+dd2MercMetersLng(lLR.x)+","+dd2MercMetersLat(lLR.y); + var lSRS="EPSG:" + this.mercatorEpsg; + } + var lURL=this.baseURL; + lURL+="?REQUEST=GetMap"; + lURL+="&SERVICE=WMS"; + lURL+="&VERSION=1.1.1"; + lURL+="&LAYERS="+this.layers; + lURL+="&STYLES="+this.styles; + lURL+="&FORMAT=image/"+this.format; + lURL+="&BGCOLOR=0xFFFFFF"; + lURL+="&TRANSPARENT=TRUE"; + lURL+="&SRS="+lSRS; + lURL+="&BBOX="+lBbox; + lURL+="&WIDTH=256"; + lURL+="&HEIGHT=256"; + lURL+="&reaspect=false"; + return lURL; +} diff --git a/public/javascripts/ym4r-gm.js b/public/javascripts/ym4r-gm.js new file mode 100644 index 0000000..1c768df --- /dev/null +++ b/public/javascripts/ym4r-gm.js @@ -0,0 +1,117 @@ +// JS helper functions for YM4R + +function addInfoWindowToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowHtml(info,options);}); + return marker; +} + +function addInfoWindowTabsToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowTabsHtml(info,options);}); + return marker; +} + +function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){ + layer.getTileUrl = getTile; + layer.getCopyright = copyright; + layer.getOpacity = opacity; + layer.isPng = isPng; + return layer; +} + +function addOptionsToIcon(icon,options){ + for(var k in options){ + icon[k] = options[k]; + } + return icon; +} + +function addCodeToFunction(func,code){ + if(func == undefined) + return code; + else{ + return function(){ + func(); + code(); + } + } +} + +function addGeocodingToMarker(marker,address){ + marker.orig_initialize = marker.initialize; + orig_redraw = marker.redraw; + marker.redraw = function(force){}; //empty the redraw method so no error when called by addOverlay. + marker.initialize = function(map){ + new GClientGeocoder().getLatLng(address, + function(latlng){ + if(latlng){ + marker.redraw = orig_redraw; + marker.orig_initialize(map); //init before setting point + marker.setPoint(latlng); + }//do nothing + }); + }; + return marker; +} + + + +GMap2.prototype.centerAndZoomOnMarkers = function(markers) { + var bounds = new GLatLngBounds(markers[0].getPoint(), + markers[0].getPoint()); + for (var i=1, len = markers.length ; i :kms, + :default_formula => :flat, + :distance_field_name => :distance + end + +You can also define alternative column names for latitude and longitude using +the :lat_column_name and :lng_column_name keys. The defaults are 'lat' and +'lng' respectively. + +Thereafter, a set of finder methods are made available. Below are the +different combinations: + +Origin as a two-element array of latititude/longitude: + + find(:all, :origin => [37.792,-122.393]) + +Origin as a geocodeable string: + + find(:all, :origin => '100 Spear st, San Francisco, CA') + +Origin as an object which responds to lat and lng methods, +or latitude and longitude methods, or whatever methods you have +specified for lng_column_name and lat_column_name: + + find(:all, :origin=>my_store) # my_store.lat and my_store.lng methods exist + +Often you will need to find within a certain distance. The prefered syntax is: + + find(:all, :origin => @somewhere, :within => 5) + +. . . however these syntaxes will also work: + + find_within(5, :origin => @somewhere) + find(:all, :origin => @somewhere, :conditions => "distance < 5") + +Note however that the third form should be avoided. With either of the first two, +GeoKit automatically adds a bounding box to speed up the radial query in the database. +With the third form, it does not. + +If you need to combine distance conditions with other conditions, you should do +so like this: + + find(:all, :origin => @somewhere, :within => 5, :conditions=>['state=?',state]) + +If :origin is not provided in the finder call, the find method +works as normal. Further, the key is removed +from the :options hash prior to invoking the superclass behavior. + +Other convenience methods work intuitively and are as follows: + + find_within(distance, :origin => @somewhere) + find_beyond(distance, :origin => @somewhere) + find_closest(:origin => @somewhere) + find_farthest(:origin => @somewhere) + +where the options respect the defaults, but can be overridden if +desired. + +Lastly, if all that is desired is the raw SQL for distance +calculations, you can use the following: + + distance_sql(origin, units=default_units, formula=default_formula) + +Thereafter, you are free to use it in find_by_sql as you wish. + +There are methods available to enable you to get the count based upon +the find condition that you have provided. These all work similarly to +the finders. So for instance: + + count(:origin, :conditions => "distance < 5") + count_within(distance, :origin => @somewhere) + count_beyond(distance, :origin => @somewhere) + +## FINDING WITHIN A BOUNDING BOX + +If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map: + + Store.find :all, :bounds=>[sw_point,ne_point] + +The input to :bounds can be array with the two points or a Bounds object. However you provide them, the order should always be the southwest corner, northeast corner of the rectangle. Typically, you will be getting the sw_point and ne_point from a map that is displayed on a web page. + +If you need to calculate the bounding box from a point and radius, you can do that: + + bounds=Bounds.from_point_and_radius(home,5) + Store.find :all, :bounds=>bounds + +## USING INCLUDES + +You can use includes along with your distance finders: + + stores=Store.find :all, :origin=>home, :include=>[:reviews,:cities] :within=>5, :order=>'distance' + +*However*, ActiveRecord drops the calculated distance column when you use include. So, if you need to +use the distance column, you'll have to re-calculate it post-query in Ruby: + + stores.sort_by_distance_from(home) + +In this case, you may want to just use the bounding box +condition alone in your SQL (there's no use calculating the distance twice): + + bounds=Bounds.from_point_and_radius(home,5) + stores=Store.find :all, :include=>[:reviews,:cities] :bounds=>bounds + stores.sort_by_distance_from(home) + +## IP GEOCODING + +You can obtain the location for an IP at any time using the geocoder +as in the following example: + + location = IpGeocoder.geocode('12.215.42.19') + +where Location is a GeoLoc instance containing the latitude, +longitude, city, state, and country code. Also, the success +value is true. + +If the IP cannot be geocoded, a GeoLoc instance is returned with a +success value of false. + +It should be noted that the IP address needs to be visible to the +Rails application. In other words, you need to ensure that the +requesting IP address is forwarded by any front-end servers that +are out in front of the Rails app. Otherwise, the IP will always +be that of the front-end server. + +## IP GEOCODING HELPER + +A class method called geocode_ip_address has been mixed into the +ActionController::Base. This enables before_filter style lookup of +the IP address. Since it is a filter, it can accept any of the +available filter options. + +Usage is as below: + + class LocationAwareController < ActionController::Base + geocode_ip_address + end + +A first-time lookup will result in the GeoLoc class being stored +in the session as :geo_location as well as in a cookie called +:geo_session. Subsequent lookups will use the session value if it +exists or the cookie value if it doesn't exist. The last resort is +to make a call to the web service. Clients are free to manage the +cookie as they wish. + +The intent of this feature is to be able to provide a good guess as +to a new visitor's location. + +## INTEGRATED FIND AND GEOCODING + +Geocoding has been integrated with the finders enabling you to pass +a physical address or an IP address. This would look the following: + + Location.find_farthest(:origin => '217.15.10.9') + Location.find_farthest(:origin => 'Irving, TX') + +where the IP or physical address would be geocoded to a location and +then the resulting latitude and longitude coordinates would be used +in the find. This is not expected to be common usage, but it can be +done nevertheless. + +## ADDRESS GEOCODING + +GeoKit can geocode addresses using multiple geocodeing web services. +Currently, GeoKit supports Google, Yahoo, and Geocoder.us geocoding +services. + +These geocoder services are made available through three classes: +GoogleGeocoder, YahooGeocoder, and UsGeocoder. Further, an additional +geocoder class called MultiGeocoder incorporates an ordered failover +sequence to increase the probability of successful geocoding. + +All classes are called using the following signature: + + include GeoKit::Geocoders + location = XxxGeocoder.geocode(address) + +where you replace Xxx Geocoder with the appropriate class. A GeoLoc +instance is the result of the call. This class has a "success" +attribute which will be true if a successful geocoding occurred. +If successful, the lat and lng properties will be populated. + +Geocoders are named with the naming convention NameGeocoder. This +naming convention enables Geocoder to auto-detect its sub-classes +in order to create methods called name_geocoder(address) so that +all geocoders are called through the base class. This is done +purely for convenience; the individual geocoder classes are expected +to be used independently. + +The MultiGeocoder class requires the configuration of a provider +order which dictates what order to use the various geocoders. Ordering +is done through the PROVIDER_ORDER constant found in environment.rb. + +On installation, this plugin appends a template for your API keys to +your environment.rb. + +Make sure your failover configuration matches the usage characteristics +of your application -- for example, if you routinely get bogus input to +geocode, your code will be much slower if you have to failover among +multiple geocoders before determining that the input was in fact bogus. + +The Geocoder.geocode method returns a GeoLoc object. Basic usage: + + loc=Geocoder.geocode('100 Spear St, San Francisco, CA') + if loc.success + puts loc.lat + puts loc.lng + puts loc.full_address + end + +## INTEGRATED FIND WITH ADDRESS GEOCODING + +Just has you can pass an IP address directly into an ActiveRecord finder +as the origin, you can also pass a physical address as the origin: + + Location.find_closest(:origin => '100 Spear st, San Francisco, CA') + +where the physical address would be geocoded to a location and then the +resulting latitude and longitude coordinates would be used in the +find. + +Note that if the address fails to geocode, the find method will raise an +ActiveRecord::GeocodeError you must be prepared to catch. Alternatively, +You can geocoder the address beforehand, and pass the resulting lat/lng +into the finder if successful. + +## Auto Geocoding + +If your geocoding needs are simple, you can tell your model to automatically +geocode itself on create: + + class Store < ActiveRecord::Base + acts_as_mappable :auto_geocode=>true + end + +It takes two optional params: + + class Store < ActiveRecord::Base + acts_as_mappable :auto_geocode=>{:field=>:address, :error_message=>'Could not geocode address'} + end + +. . . which is equivilent to: + + class Store << ActiveRecord::Base + acts_as_mappable + before_validation_on_create :geocode_address + + private + def geocode_address + geo=GeoKit::Geocoders::MultiGeocoder.geocode (address) + errors.add(:address, "Could not Geocode address") if !geo.success + self.lat, self.lng = geo.lat,geo.lng if geo.success + end + end + +If you need any more complicated geocoding behavior for your model, you should roll your own +before_validate callback. + + +## Distances, headings, endpoints, and midpoints + + distance=home.distance_from(work, :units=>:miles) + heading=home.heading_to(work) # result is in degrees, 0 is north + endpoint=home.endpoint(90,2) # two miles due east + midpoing=home.midpoint_to(work) + +## Cool stuff you can do with bounds + + bounds=Bounds.new(sw_point,ne_point) + bounds.contains?(home) + puts bounds.center + + +HOW TO . . . +================================================================================= + +## How to install the GeoKit plugin + cd [APP_ROOT] + ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk + or, to install as an external (your project must be version controlled): + ruby script/plugin install -x svn://rubyforge.org/var/svn/geokit/trunk + +## How to find all stores within a 10-mile radius of a given lat/lng +1. ensure your stores table has lat and lng columns with numeric or float + datatypes to store your latitude/longitude + +3. use acts_as_mappable on your store model: + class Store < ActiveRecord::Base + acts_as_mappable + ... + end +3. finders now have extra capabilities: + Store.find(:all, :origin =>[32.951613,-96.958444], :within=>10) + +## How to geocode an address + +1. configure your geocoder key(s) in environment.rb + +2. also in environment.rb, make sure that PROVIDER_ORDER reflects the + geocoder(s). If you only want to use one geocoder, there should + be only one symbol in the array. For example: + PROVIDER_ORDER=[:google] + +3. Test it out in script/console + include GeoKit::Geocoders + res = MultiGeocoder.geocode('100 Spear St, San Francisco, CA') + puts res.lat + puts res.lng + puts res.full_address + ... etc. The return type is GeoLoc, see the API for + all the methods you can call on it. + +## How to find all stores within 10 miles of a given address + +1. as above, ensure your table has the lat/lng columns, and you've + applied acts_as_mappable to the Store model. + +2. configure and test out your geocoder, as above + +3. pass the address in under the :origin key + Store.find(:all, :origin=>'100 Spear st, San Francisco, CA', + :within=>10) + +4. you can also use a zipcode, or anything else that's geocodable: + Store.find(:all, :origin=>'94117', + :conditions=>'distance<10') + +## How to sort a query by distance from an origin + +You now have access to a 'distance' column, and you can use it +as you would any other column. For example: + Store.find(:all, :origin=>'94117', :order=>'distance') + +## How to elements of an array according to distance from a common point + +Usually, you can do your sorting in the database as part of your find call. +If you need to sort things post-query, you can do so: + + stores=Store.find :all + stores.sort_by_distance_from(home) + puts stores.first.distance + +Obviously, each of the items in the array must have a latitude/longitude so +they can be sorted by distance. + + +HIGH-LEVEL NOTES ON WHAT'S WHERE +================================================================================= + +acts_as_mappable.rb, as you'd expect, contains the ActsAsMappable +module which gets mixed into your models to provide the +location-based finder goodness. + +mappable.rb contains the Mappable module, which provides basic +distance calculation methods, i.e., calculating the distance +between two points. + +mappable.rb also contains LatLng, GeoLoc, and Bounds. +LatLng is a simple container for latitude and longitude, but +it's made more powerful by mixing in the above-mentioned Mappable +module -- therefore, you can calculate easily the distance between two +LatLng ojbects with distance = first.distance_to(other) + +GeoLoc (also in mappable.rb) represents an address or location which +has been geocoded. You can get the city, zipcode, street address, etc. +from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng +AND the Mappable modeule goodness for free. + +geocoders.rb contains the geocoder classes. + +ip_geocode_lookup.rb contains the before_filter helper method which +enables auto lookup of the requesting IP address. + +## IMPORTANT NOTE: We have appended to your environment.rb file + +Installation of this plugin has appended an API key template +to your environment.rb file. You *must* add your own keys for the various +geocoding services if you want to use geocoding. If you need to refer to the original +template again, see the api_keys_template file in the root of the plugin. diff --git a/vendor/plugins/geokit/Rakefile b/vendor/plugins/geokit/Rakefile new file mode 100644 index 0000000..82e289e --- /dev/null +++ b/vendor/plugins/geokit/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the GeoKit plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the GeoKit plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'GeoKit' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end \ No newline at end of file diff --git a/vendor/plugins/geokit/about.yml b/vendor/plugins/geokit/about.yml new file mode 100644 index 0000000..7f759fd --- /dev/null +++ b/vendor/plugins/geokit/about.yml @@ -0,0 +1,9 @@ +author: + name_1: Bill Eisenhauer + homepage_1: http://blog.billeisenhauer.com + name_2: Andre Lewis + homepage_2: http://www.earthcode.com +summary: Geo distance calculations, distance calculation query support, geocoding for physical and ip addresses. +version: 1.0 +rails_version: 1.0+ +license: MIT \ No newline at end of file diff --git a/vendor/plugins/geokit/assets/api_keys_template b/vendor/plugins/geokit/assets/api_keys_template new file mode 100644 index 0000000..6367a1a --- /dev/null +++ b/vendor/plugins/geokit/assets/api_keys_template @@ -0,0 +1,50 @@ +# These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable +GeoKit::default_units = :miles +GeoKit::default_formula = :sphere + +# This is the timeout value in seconds to be used for calls to the geocoder web +# services. For no timeout at all, comment out the setting. The timeout unit +# is in seconds. +GeoKit::Geocoders::timeout = 3 + +# These settings are used if web service calls must be routed through a proxy. +# These setting can be nil if not needed, otherwise, addr and port must be +# filled in at a minimum. If the proxy requires authentication, the username +# and password can be provided as well. +GeoKit::Geocoders::proxy_addr = nil +GeoKit::Geocoders::proxy_port = nil +GeoKit::Geocoders::proxy_user = nil +GeoKit::Geocoders::proxy_pass = nil + +# This is your yahoo application key for the Yahoo Geocoder. +# See http://developer.yahoo.com/faq/index.html#appid +# and http://developer.yahoo.com/maps/rest/V1/geocode.html +GeoKit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' + +# This is your Google Maps geocoder key. +# See http://www.google.com/apis/maps/signup.html +# and http://www.google.com/apis/maps/documentation/#Geocoding_Examples +GeoKit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' + +# This is your username and password for geocoder.us. +# To use the free service, the value can be set to nil or false. For +# usage tied to an account, the value should be set to username:password. +# See http://geocoder.us +# and http://geocoder.us/user/signup +GeoKit::Geocoders::geocoder_us = false + +# This is your authorization key for geocoder.ca. +# To use the free service, the value can be set to nil or false. For +# usage tied to an account, set the value to the key obtained from +# Geocoder.ca. +# See http://geocoder.ca +# and http://geocoder.ca/?register=1 +GeoKit::Geocoders::geocoder_ca = false + +# This is the order in which the geocoders are called in a failover scenario +# If you only want to use a single geocoder, put a single symbol in the array. +# Valid symbols are :google, :yahoo, :us, and :ca. +# Be aware that there are Terms of Use restrictions on how you can use the +# various geocoders. Make sure you read up on relevant Terms of Use for each +# geocoder you are going to use. +GeoKit::Geocoders::provider_order = [:google,:us] \ No newline at end of file diff --git a/vendor/plugins/geokit/init.rb b/vendor/plugins/geokit/init.rb new file mode 100644 index 0000000..d36503e --- /dev/null +++ b/vendor/plugins/geokit/init.rb @@ -0,0 +1,13 @@ +# Load modules and classes needed to automatically mix in ActiveRecord and +# ActionController helpers. All other functionality must be explicitly +# required. +require 'geo_kit/defaults' +require 'geo_kit/mappable' +require 'geo_kit/acts_as_mappable' +require 'geo_kit/ip_geocode_lookup' + +# Automatically mix in distance finder support into ActiveRecord classes. +ActiveRecord::Base.send :include, GeoKit::ActsAsMappable + +# Automatically mix in ip geocoding helpers into ActionController classes. +ActionController::Base.send :include, GeoKit::IpGeocodeLookup diff --git a/vendor/plugins/geokit/install.rb b/vendor/plugins/geokit/install.rb new file mode 100644 index 0000000..8263e22 --- /dev/null +++ b/vendor/plugins/geokit/install.rb @@ -0,0 +1,7 @@ +# Display to the console the contents of the README file. +puts IO.read(File.join(File.dirname(__FILE__), 'README')) + +# Append the contents of api_keys_template to the application's environment.rb file +environment_rb = File.open(File.expand_path(File.join(File.dirname(__FILE__), '../../../config/environment.rb')), "a") +environment_rb.puts IO.read(File.join(File.dirname(__FILE__), '/assets/api_keys_template')) +environment_rb.close diff --git a/vendor/plugins/geokit/lib/geo_kit/acts_as_mappable.rb b/vendor/plugins/geokit/lib/geo_kit/acts_as_mappable.rb new file mode 100644 index 0000000..386b05e --- /dev/null +++ b/vendor/plugins/geokit/lib/geo_kit/acts_as_mappable.rb @@ -0,0 +1,436 @@ +module GeoKit + # Contains the class method acts_as_mappable targeted to be mixed into ActiveRecord. + # When mixed in, augments find services such that they provide distance calculation + # query services. The find method accepts additional options: + # + # * :origin - can be + # 1. a two-element array of latititude/longitude -- :origin=>[37.792,-122.393] + # 2. a geocodeable string -- :origin=>'100 Spear st, San Francisco, CA' + # 3. an object which responds to lat and lng methods, or latitude and longitude methods, + # or whatever methods you have specified for lng_column_name and lat_column_name + # + # Other finder methods are provided for specific queries. These are: + # + # * find_within (alias: find_inside) + # * find_beyond (alias: find_outside) + # * find_closest (alias: find_nearest) + # * find_farthest + # + # Counter methods are available and work similarly to finders. + # + # If raw SQL is desired, the distance_sql method can be used to obtain SQL appropriate + # to use in a find_by_sql call. + module ActsAsMappable + # Mix below class methods into ActiveRecord. + def self.included(base) # :nodoc: + base.extend ClassMethods + end + + # Class method to mix into active record. + module ClassMethods # :nodoc: + # Class method to bring distance query support into ActiveRecord models. By default + # uses :miles for distance units and performs calculations based upon the Haversine + # (sphere) formula. These can be changed by setting GeoKit::default_units and + # GeoKit::default_formula. Also, by default, uses lat, lng, and distance for respective + # column names. All of these can be overridden using the :default_units, :default_formula, + # :lat_column_name, :lng_column_name, and :distance_column_name hash keys. + # + # Can also use to auto-geocode a specific column on create. Syntax; + # + # acts_as_mappable :auto_geocode=>true + # + # By default, it tries to geocode the "address" field. Or, for more customized behavior: + # + # acts_as_mappable :auto_geocode=>{:field=>:address,:error_message=>'bad address'} + # + # In both cases, it creates a before_validation_on_create callback to geocode the given column. + # For anything more customized, we recommend you forgo the auto_geocode option + # and create your own AR callback to handle geocoding. + def acts_as_mappable(options = {}) + # Mix in the module, but ensure to do so just once. + return if self.included_modules.include?(GeoKit::ActsAsMappable::InstanceMethods) + send :include, GeoKit::ActsAsMappable::InstanceMethods + # include the Mappable module. + send :include, Mappable + + # Handle class variables. + cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name + self.distance_column_name = options[:distance_column_name] || 'distance' + self.default_units = options[:default_units] || GeoKit::default_units + self.default_formula = options[:default_formula] || GeoKit::default_formula + self.lat_column_name = options[:lat_column_name] || 'lat' + self.lng_column_name = options[:lng_column_name] || 'lng' + self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}" + self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}" + if options.include?(:auto_geocode) && options[:auto_geocode] + # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash + options[:auto_geocode] = {} if options[:auto_geocode] == true + cattr_accessor :auto_geocode_field, :auto_geocode_error_message + self.auto_geocode_field = options[:auto_geocode][:field] || 'address' + self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address' + + # set the actual callback here + before_validation_on_create :auto_geocode_address + end + end + end + + # this is the callback for auto_geocoding + def auto_geocode_address + address=self.send(auto_geocode_field) + geo=GeoKit::Geocoders::MultiGeocoder.geocode(address) + + if geo.success + self.send("#{lat_column_name}=", geo.lat) + self.send("#{lng_column_name}=", geo.lng) + else + errors.add(auto_geocode_field, auto_geocode_error_message) + end + + geo.success + end + + # Instance methods to mix into ActiveRecord. + module InstanceMethods #:nodoc: + # Mix class methods into module. + def self.included(base) # :nodoc: + base.extend SingletonMethods + end + + # Class singleton methods to mix into ActiveRecord. + module SingletonMethods # :nodoc: + # Extends the existing find method in potentially two ways: + # - If a mappable instance exists in the options, adds a distance column. + # - If a mappable instance exists in the options and the distance column exists in the + # conditions, substitutes the distance sql for the distance column -- this saves + # having to write the gory SQL. + def find(*args) + prepare_for_find_or_count(:find, args) + super(*args) + end + + # Extends the existing count method by: + # - If a mappable instance exists in the options and the distance column exists in the + # conditions, substitutes the distance sql for the distance column -- this saves + # having to write the gory SQL. + def count(*args) + prepare_for_find_or_count(:count, args) + super(*args) + end + + # Finds within a distance radius. + def find_within(distance, options={}) + options[:within] = distance + find(:all, options) + end + alias find_inside find_within + + # Finds beyond a distance radius. + def find_beyond(distance, options={}) + options[:beyond] = distance + find(:all, options) + end + alias find_outside find_beyond + + # Finds according to a range. Accepts inclusive or exclusive ranges. + def find_by_range(range, options={}) + options[:range] = range + find(:all, options) + end + + # Finds the closest to the origin. + def find_closest(options={}) + find(:nearest, options) + end + alias find_nearest find_closest + + # Finds the farthest from the origin. + def find_farthest(options={}) + find(:farthest, options) + end + + # Finds within rectangular bounds (sw,ne). + def find_within_bounds(bounds, options={}) + options[:bounds] = bounds + find(:all, options) + end + + # counts within a distance radius. + def count_within(distance, options={}) + options[:within] = distance + count(options) + end + alias count_inside count_within + + # Counts beyond a distance radius. + def count_beyond(distance, options={}) + options[:beyond] = distance + count(options) + end + alias count_outside count_beyond + + # Counts according to a range. Accepts inclusive or exclusive ranges. + def count_by_range(range, options={}) + options[:range] = range + count(options) + end + + # Finds within rectangular bounds (sw,ne). + def count_within_bounds(bounds, options={}) + options[:bounds] = bounds + count(options) + end + + # Returns the distance calculation to be used as a display column or a condition. This + # is provide for anyone wanting access to the raw SQL. + def distance_sql(origin, units=default_units, formula=default_formula) + case formula + when :sphere + sql = sphere_distance_sql(origin, units) + when :flat + sql = flat_distance_sql(origin, units) + end + sql + end + + private + + # Prepares either a find or a count action by parsing through the options and + # conditionally adding to the select clause for finders. + def prepare_for_find_or_count(action, args) + options = extract_options_from_args!(args) + # Obtain items affecting distance condition. + origin = extract_origin_from_options(options) + units = extract_units_from_options(options) + formula = extract_formula_from_options(options) + bounds = extract_bounds_from_options(options) + # if no explicit bounds were given, try formulating them from the point and distance given + bounds = formulate_bounds_from_distance(options, origin, units) unless bounds + # Apply select adjustments based upon action. + add_distance_to_select(options, origin, units, formula) if origin && action == :find + # Apply the conditions for a bounding rectangle if applicable + apply_bounds_conditions(options,bounds) if bounds + # Apply distance scoping and perform substitutions. + apply_distance_scope(options) + substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions) + # Order by scoping for find action. + apply_find_scope(args, options) if action == :find + # Unfortunatley, we need to do extra work if you use an :include. See the method for more info. + handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin + # Restore options minus the extra options that we used for the + # GeoKit API. + args.push(options) + end + + # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied. + # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include, + # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select. + # So, the 'distance' column isn't available for the :order clause to reference when we use :include. + def handle_order_with_include(options, origin, units, formula) + # replace the distance_column_name with the distance sql in order clause + options[:order].sub!(distance_column_name, distance_sql(origin, units, formula)) + end + + # Looks for mapping-specific tokens and makes appropriate translations so that the + # original finder has its expected arguments. Resets the the scope argument to + # :first and ensures the limit is set to one. + def apply_find_scope(args, options) + case args.first + when :nearest + args[0] = :first + options[:limit] = 1 + options[:order] = "#{distance_column_name} ASC" + when :farthest + args[0] = :first + options[:limit] = 1 + options[:order] = "#{distance_column_name} DESC" + end + end + + # If it's a :within query, add a bounding box to improve performance. + # This only gets called if a :bounds argument is not otherwise supplied. + def formulate_bounds_from_distance(options, origin, units) + distance = options[:within] if options.has_key?(:within) + distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range) + if distance + res=GeoKit::Bounds.from_point_and_radius(origin,distance,:units=>units) + else + nil + end + end + + # Replace :within, :beyond and :range distance tokens with the appropriate distance + # where clauses. Removes these tokens from the options hash. + def apply_distance_scope(options) + distance_condition = "#{distance_column_name} <= #{options[:within]}" if options.has_key?(:within) + distance_condition = "#{distance_column_name} > #{options[:beyond]}" if options.has_key?(:beyond) + distance_condition = "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}" if options.has_key?(:range) + [:within, :beyond, :range].each { |option| options.delete(option) } if distance_condition + + options[:conditions]=augment_conditions(options[:conditions],distance_condition) if distance_condition + end + + # This method lets you transparently add a new condition to a query without + # worrying about whether it currently has conditions, or what kind of conditions they are + # (string or array). + # + # Takes the current conditions (which can be an array or a string, or can be nil/false), + # and a SQL string. It inserts the sql into the existing conditions, and returns new conditions + # (which can be a string or an array + def augment_conditions(current_conditions,sql) + if current_conditions && current_conditions.is_a?(String) + res="#{current_conditions} AND #{sql}" + elsif current_conditions && current_conditions.is_a?(Array) + current_conditions[0]="#{current_conditions[0]} AND #{sql}" + res=current_conditions + else + res=sql + end + res + end + + # Alters the conditions to include rectangular bounds conditions. + def apply_bounds_conditions(options,bounds) + sw,ne=bounds.sw,bounds.ne + lng_sql= bounds.crosses_meridian? ? "#{qualified_lng_column_name}<#{sw.lng} OR #{qualified_lng_column_name}>#{ne.lng}" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}" + bounds_sql="#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}" + options[:conditions]=augment_conditions(options[:conditions],bounds_sql) + end + + # Extracts the origin instance out of the options if it exists and returns + # it. If there is no origin, looks for latitude and longitude values to + # create an origin. The side-effect of the method is to remove these + # option keys from the hash. + def extract_origin_from_options(options) + origin = options.delete(:origin) + res = normalize_point_to_lat_lng(origin) if origin + res + end + + # Extract the units out of the options if it exists and returns it. If + # there is no :units key, it uses the default. The side effect of the + # method is to remove the :units key from the options hash. + def extract_units_from_options(options) + units = options[:units] || default_units + options.delete(:units) + units + end + + # Extract the formula out of the options if it exists and returns it. If + # there is no :formula key, it uses the default. The side effect of the + # method is to remove the :formula key from the options hash. + def extract_formula_from_options(options) + formula = options[:formula] || default_formula + options.delete(:formula) + formula + end + + def extract_bounds_from_options(options) + bounds = options.delete(:bounds) + bounds = GeoKit::Bounds.normalize(bounds) if bounds + end + + # Geocode IP address. + def geocode_ip_address(origin) + geo_location = GeoKit::Geocoders::IpGeocoder.geocode(origin) + return geo_location if geo_location.success + raise GeoKit::Geocoders::GeocodeError + end + + + # Given a point in a variety of (an address to geocode, + # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres) + # this method will normalize it into a GeoKit::LatLng instance. The only thing this + # method adds on top of LatLng#normalize is handling of IP addresses + def normalize_point_to_lat_lng(point) + res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point) + res = GeoKit::LatLng.normalize(point) unless res + res + end + + # Augments the select with the distance SQL. + def add_distance_to_select(options, origin, units=default_units, formula=default_formula) + if origin + distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}" + selector = options.has_key?(:select) && options[:select] ? options[:select] : "*" + options[:select] = "#{selector}, #{distance_selector}" + end + end + + # Looks for the distance column and replaces it with the distance sql. If an origin was not + # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database. + # Conditions are either a string or an array. In the case of an array, the first entry contains + # the condition. + def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula) + original_conditions = options[:conditions] + condition = original_conditions.is_a?(String) ? original_conditions : original_conditions.first + pattern = Regexp.new("\s*#{distance_column_name}(\s<>=)*") + condition = condition.gsub(pattern, distance_sql(origin, units, formula)) + original_conditions = condition if original_conditions.is_a?(String) + original_conditions[0] = condition if original_conditions.is_a?(Array) + options[:conditions] = original_conditions + end + + # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned + # to the database in use. + def sphere_distance_sql(origin, units) + lat = deg2rad(origin.lat) + lng = deg2rad(origin.lng) + multiplier = units_sphere_multiplier(units) + case connection.adapter_name.downcase + when "mysql" + sql=<<-SQL_END + (ACOS(COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ + COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ + SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name})))*#{multiplier}) + SQL_END + when "postgresql" + sql=<<-SQL_END + (ACOS(COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ + COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ + SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name})))*#{multiplier}) + SQL_END + else + sql = "unhandled #{connection.adapter_name.downcase} adapter" + end + end + + # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned + # to the database in use. + def flat_distance_sql(origin, units) + lat_degree_units = units_per_latitude_degree(units) + lng_degree_units = units_per_longitude_degree(origin.lat, units) + case connection.adapter_name.downcase + when "mysql" + sql=<<-SQL_END + SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ + POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) + SQL_END + when "postgresql" + sql=<<-SQL_END + SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ + POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) + SQL_END + else + sql = "unhandled #{connection.adapter_name.downcase} adapter" + end + end + end + end + end +end + +# Extend Array with a sort_by_distance method. +# This method creates a "distance" attribute on each object, +# calculates the distance from the passed origin, +# and finally sorts the array by the resulting distance. +class Array + def sort_by_distance_from(origin, opts={}) + distance_attribute_name = opts.delete(:distance_attribute_name) || 'distance' + self.each do |e| + e.class.send(:attr_accessor, distance_attribute_name) if !e.respond_to? "#{distance_attribute_name}=" + e.send("#{distance_attribute_name}=", origin.distance_to(e,opts)) + end + self.sort!{|a,b|a.send(distance_attribute_name) <=> b.send(distance_attribute_name)} + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/lib/geo_kit/defaults.rb b/vendor/plugins/geokit/lib/geo_kit/defaults.rb new file mode 100644 index 0000000..e9832c1 --- /dev/null +++ b/vendor/plugins/geokit/lib/geo_kit/defaults.rb @@ -0,0 +1,21 @@ +module GeoKit + # These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable + @@default_units = :miles + @@default_formula = :sphere + + [:default_units, :default_formula].each do |sym| + class_eval <<-EOS, __FILE__, __LINE__ + def self.#{sym} + if defined?(#{sym.to_s.upcase}) + #{sym.to_s.upcase} + else + @@#{sym} + end + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + EOS + end +end diff --git a/vendor/plugins/geokit/lib/geo_kit/geocoders.rb b/vendor/plugins/geokit/lib/geo_kit/geocoders.rb new file mode 100644 index 0000000..ccf3ab4 --- /dev/null +++ b/vendor/plugins/geokit/lib/geo_kit/geocoders.rb @@ -0,0 +1,347 @@ +require 'net/http' +require 'rexml/document' +require 'yaml' +require 'timeout' + +module GeoKit + # Contains a set of geocoders which can be used independently if desired. The list contains: + # + # * Google Geocoder - requires an API key. + # * Yahoo Geocoder - requires an API key. + # * Geocoder.us - may require authentication if performing more than the free request limit. + # * Geocoder.ca - for Canada; may require authentication as well. + # * IP Geocoder - geocodes an IP address using hostip.info's web service. + # * Multi Geocoder - provides failover for the physical location geocoders. + # + # Some configuration is required for these geocoders and can be located in the environment + # configuration files. + module Geocoders + @@proxy_addr = nil + @@proxy_port = nil + @@proxy_user = nil + @@proxy_pass = nil + @@timeout = nil + @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' + @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' + @@geocoder_us = false + @@geocoder_ca = false + @@provider_order = [:google,:us] + + [:yahoo, :google, :geocoder_us, :geocoder_ca, :provider_order, :timeout, + :proxy_addr, :proxy_port, :proxy_user, :proxy_pass].each do |sym| + class_eval <<-EOS, __FILE__, __LINE__ + def self.#{sym} + if defined?(#{sym.to_s.upcase}) + #{sym.to_s.upcase} + else + @@#{sym} + end + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + EOS + end + + # Error which is thrown in the event a geocoding error occurs. + class GeocodeError < StandardError; end + + # The Geocoder base class which defines the interface to be used by all + # other geocoders. + class Geocoder + # Main method which calls the do_geocode template method which subclasses + # are responsible for implementing. Returns a populated GeoLoc or an + # empty one with a failed success code. + def self.geocode(address) + res = do_geocode(address) + return res.success ? res : GeoLoc.new + end + + # Call the geocoder service using the timeout if configured. + def self.call_geocoder_service(url) + timeout(GeoKit::Geocoders::timeout) { return self.do_get(url) } if GeoKit::Geocoders::timeout + return self.do_get(url) + rescue TimeoutError + return nil + end + + protected + + def self.logger() RAILS_DEFAULT_LOGGER; end + + private + + # Wraps the geocoder call around a proxy if necessary. + def self.do_get(url) + return Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr, GeoKit::Geocoders::proxy_port, + GeoKit::Geocoders::proxy_user, GeoKit::Geocoders::proxy_pass).get_response(URI.parse(url)) + end + + # Adds subclass' geocode method making it conveniently available through + # the base class. + def self.inherited(clazz) + class_name = clazz.name.split('::').last + src = <<-END_SRC + def self.#{class_name.underscore}(address) + #{class_name}.geocode(address) + end + END_SRC + class_eval(src) + end + end + + # Geocoder CA geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_CA variable to + # contain true or false based upon whether authentication is to occur. Conforms to the + # interface set by the Geocoder class. + # + # Returns a response like: + # + # + # 49.243086 + # -123.153684 + # + class CaGeocoder < Geocoder + + private + + # Template method which does the geocode lookup. + def self.do_geocode(address) + raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc) + url = construct_request(address) + res = self.call_geocoder_service(url) + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) + xml = res.body + logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}" + # Parse the document. + doc = REXML::Document.new(xml) + address.lat = doc.elements['//latt'].text + address.lng = doc.elements['//longt'].text + address.success = true + return address + rescue + logger.error "Caught an error during Geocoder.ca geocoding call: "+$! + return GeoLoc.new + end + + # Formats the request in the format acceptable by the CA geocoder. + def self.construct_request(location) + url = "" + url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address + url += add_ampersand(url) + "addresst=#{CGI.escape(location.street_name)}" if location.street_address + url += add_ampersand(url) + "city=#{CGI.escape(location.city)}" if location.city + url += add_ampersand(url) + "prov=#{location.state}" if location.state + url += add_ampersand(url) + "postal=#{location.zip}" if location.zip + url += add_ampersand(url) + "auth=#{GeoKit::Geocoders::geocoder_ca}" if GeoKit::Geocoders::geocoder_ca + url += add_ampersand(url) + "geoit=xml" + 'http://geocoder.ca/?' + url + end + + def self.add_ampersand(url) + url && url.length > 0 ? "&" : "" + end + end + + # Google geocoder implementation. Requires the GeoKit::Geocoders::GOOGLE variable to + # contain a Google API key. Conforms to the interface set by the Geocoder class. + class GoogleGeocoder < Geocoder + + private + + # Template method which does the geocode lookup. + def self.do_geocode(address) + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address + res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8") +# res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8")) + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) + xml=res.body + logger.debug "Google geocoding. Address: #{address}. Result: #{xml}" + doc=REXML::Document.new(xml) + + if doc.elements['//kml/Response/Status/code'].text == '200' + res = GeoLoc.new + coordinates=doc.elements['//coordinates'].text.to_s.split(',') + + #basics + res.lat=coordinates[1] + res.lng=coordinates[0] + res.country_code=doc.elements['//CountryNameCode'].text + res.provider='google' + + #extended -- false if not not available + res.city = doc.elements['//LocalityName'].text if doc.elements['//LocalityName'] + res.state = doc.elements['//AdministrativeAreaName'].text if doc.elements['//AdministrativeAreaName'] + res.full_address = doc.elements['//address'].text if doc.elements['//address'] # google provides it + res.zip = doc.elements['//PostalCodeNumber'].text if doc.elements['//PostalCodeNumber'] + res.street_address = doc.elements['//ThoroughfareName'].text if doc.elements['//ThoroughfareName'] + # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country + # For Google, 1=low accuracy, 8=high accuracy + address_details=doc.elements['//AddressDetails','urn:oasis:names:tc:ciq:xsdschema:xAL:2.0'] + accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0 + res.precision=%w{unknown country state state city zip zip+4 street address}[accuracy] + res.success=true + + return res + else + logger.info "Google was unable to geocode address: "+address + return GeoLoc.new + end + + rescue + logger.error "Caught an error during Google geocoding call: "+$! + return GeoLoc.new + end + end + + # Provides geocoding based upon an IP address. The underlying web service is a hostip.info + # which sources their data through a combination of publicly available information as well + # as community contributions. + class IpGeocoder < Geocoder + + private + + # Given an IP address, returns a GeoLoc instance which contains latitude, + # longitude, city, and country code. Sets the success attribute to false if the ip + # parameter does not match an ip address. + def self.do_geocode(ip) + return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) + url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true" + response = self.call_geocoder_service(url) + response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new + rescue + logger.error "Caught an error during HostIp geocoding call: "+$! + return GeoLoc.new + end + + # Converts the body to YAML since its in the form of: + # + # Country: UNITED STATES (US) + # City: Sugar Grove, IL + # Latitude: 41.7696 + # Longitude: -88.4588 + # + # then instantiates a GeoLoc instance to populate with location data. + def self.parse_body(body) # :nodoc: + yaml = YAML.load(body) + res = GeoLoc.new + res.provider = 'hostip' + res.city, res.state = yaml['City'].split(', ') + country, res.country_code = yaml['Country'].split(' (') + res.lat = yaml['Latitude'] + res.lng = yaml['Longitude'] + res.country_code.chop! + res.success = res.city != "(Private Address)" + res + end + end + + # Geocoder Us geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_US variable to + # contain true or false based upon whether authentication is to occur. Conforms to the + # interface set by the Geocoder class. + class UsGeocoder < Geocoder + + private + + # For now, the geocoder_method will only geocode full addresses -- not zips or cities in isolation + def self.do_geocode(address) + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address + url = "http://"+(GeoKit::Geocoders::geocoder_us || '')+"geocoder.us/service/csv/geocode?address=#{CGI.escape(address_str)}" + res = self.call_geocoder_service(url) + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) + data = res.body + logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}" + array = data.chomp.split(',') + + if array.length == 6 + res=GeoLoc.new + res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array + res.country_code='US' + res.success=true + return res + else + logger.info "geocoder.us was unable to geocode address: "+address + return GeoLoc.new + end + rescue + logger.error "Caught an error during geocoder.us geocoding call: "+$! + return GeoLoc.new + end + end + + # Yahoo geocoder implementation. Requires the GeoKit::Geocoders::YAHOO variable to + # contain a Yahoo API key. Conforms to the interface set by the Geocoder class. + class YahooGeocoder < Geocoder + + private + + # Template method which does the geocode lookup. + def self.do_geocode(address) + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address + url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{GeoKit::Geocoders::yahoo}&location=#{CGI.escape(address_str)}" + res = self.call_geocoder_service(url) + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) + xml = res.body + doc = REXML::Document.new(xml) + logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}" + + if doc.elements['//ResultSet'] + res=GeoLoc.new + + #basic + res.lat=doc.elements['//Latitude'].text + res.lng=doc.elements['//Longitude'].text + res.country_code=doc.elements['//Country'].text + res.provider='yahoo' + + #extended - false if not available + res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil + res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil + res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil + res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil + res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result'] + res.success=true + return res + else + logger.info "Yahoo was unable to geocode address: "+address + return GeoLoc.new + end + + rescue + logger.info "Caught an error during Yahoo geocoding call: "+$! + return GeoLoc.new + end + end + + # Provides methods to geocode with a variety of geocoding service providers, plus failover + # among providers in the order you configure. + # + # Goal: + # - homogenize the results of multiple geocoders + # + # Limitations: + # - currently only provides the first result. Sometimes geocoders will return multiple results. + # - currently discards the "accuracy" component of the geocoding calls + class MultiGeocoder < Geocoder + private + + # This method will call one or more geocoders in the order specified in the + # configuration until one of the geocoders work. + # + # The failover approach is crucial for production-grade apps, but is rarely used. + # 98% of your geocoding calls will be successful with the first call + def self.do_geocode(address) + GeoKit::Geocoders::provider_order.each do |provider| + begin + klass = GeoKit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder" + res = klass.send :geocode, address + return res if res.success + rescue + logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in GeoKit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}") + end + end + # If we get here, we failed completely. + GeoLoc.new + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/lib/geo_kit/ip_geocode_lookup.rb b/vendor/plugins/geokit/lib/geo_kit/ip_geocode_lookup.rb new file mode 100644 index 0000000..f43edf4 --- /dev/null +++ b/vendor/plugins/geokit/lib/geo_kit/ip_geocode_lookup.rb @@ -0,0 +1,46 @@ +require 'yaml' + +module GeoKit + # Contains a class method geocode_ip_address which can be used to enable automatic geocoding + # for request IP addresses. The geocoded information is stored in a cookie and in the + # session to minimize web service calls. The point of the helper is to enable location-based + # websites to have a best-guess for new visitors. + module IpGeocodeLookup + # Mix below class methods into ActionController. + def self.included(base) # :nodoc: + base.extend ClassMethods + end + + # Class method to mix into active record. + module ClassMethods # :nodoc: + def geocode_ip_address(filter_options = {}) + before_filter :store_ip_location + end + end + + private + + # Places the IP address' geocode location into the session if it + # can be found. Otherwise, looks for a geo location cookie and + # uses that value. The last resort is to call the web service to + # get the value. + def store_ip_location + session[:geo_location] ||= retrieve_location_from_cookie_or_service + cookies[:geo_location] = { :value => session[:geo_location].to_yaml, :expires => 30.days.from_now } if session[:geo_location] + end + + # Uses the stored location value from the cookie if it exists. If + # no cookie exists, calls out to the web service to get the location. + def retrieve_location_from_cookie_or_service + return YAML.load(cookies[:geo_location]) if cookies[:geo_location] + location = Geocoders::IpGeocoder.geocode(get_ip_address) + return location.success ? location : nil + end + + # Returns the real ip address, though this could be the localhost ip + # address. No special handling here anymore. + def get_ip_address + request.remote_ip + end + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/lib/geo_kit/mappable.rb b/vendor/plugins/geokit/lib/geo_kit/mappable.rb new file mode 100644 index 0000000..a7a1030 --- /dev/null +++ b/vendor/plugins/geokit/lib/geo_kit/mappable.rb @@ -0,0 +1,431 @@ +require 'geo_kit/defaults' + +module GeoKit + # Contains class and instance methods providing distance calcuation services. This + # module is meant to be mixed into classes containing lat and lng attributes where + # distance calculation is desired. + # + # At present, two forms of distance calculations are provided: + # + # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances. + # * Haversine (sphere) - which is fairly accurate, but at a performance cost. + # + # Distance units supported are :miles and :kms. + module Mappable + PI_DIV_RAD = 0.0174 + KMS_PER_MILE = 1.609 + EARTH_RADIUS_IN_MILES = 3963.19 + EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE + MILES_PER_LATITUDE_DEGREE = 69.1 + KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE + LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE + + # Mix below class methods into the includer. + def self.included(receiver) # :nodoc: + receiver.extend ClassMethods + end + + module ClassMethods #:nodoc: + # Returns the distance between two points. The from and to parameters are + # required to have lat and lng attributes. Valid options are: + # :units - valid values are :miles or :kms (GeoKit::default_units is the default) + # :formula - valid values are :flat or :sphere (GeoKit::default_formula is the default) + def distance_between(from, to, options={}) + from=GeoKit::LatLng.normalize(from) + to=GeoKit::LatLng.normalize(to) + units = options[:units] || GeoKit::default_units + formula = options[:formula] || GeoKit::default_formula + case formula + when :sphere + units_sphere_multiplier(units) * + Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) + + Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) * + Math.cos(deg2rad(to.lng) - deg2rad(from.lng))) + when :flat + Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 + + (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2) + end + end + + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) + # from the first point to the second point. Typicaly, the instance methods will be used + # instead of this method. + def heading_between(from,to) + from=GeoKit::LatLng.normalize(from) + to=GeoKit::LatLng.normalize(to) + + d_lng=deg2rad(to.lng-from.lng) + from_lat=deg2rad(from.lat) + to_lat=deg2rad(to.lat) + y=Math.sin(d_lng) * Math.cos(to_lat) + x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng) + heading=to_heading(Math.atan2(y,x)) + end + + # Given a start point, distance, and heading (in degrees), provides + # an endpoint. Returns a LatLng instance. Typically, the instance method + # will be used instead of this method. + def endpoint(start,heading, distance, options={}) + units = options[:units] || GeoKit::default_units + radius = units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS + start=GeoKit::LatLng.normalize(start) + lat=deg2rad(start.lat) + lng=deg2rad(start.lng) + heading=deg2rad(heading) + distance=distance.to_f + + end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) + + Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading)) + + end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat), + Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat)) + + LatLng.new(rad2deg(end_lat),rad2deg(end_lng)) + end + + # Returns the midpoint, given two points. Returns a LatLng. + # Typically, the instance method will be used instead of this method. + # Valid option: + # :units - valid values are :miles or :kms (:miles is the default) + def midpoint_between(from,to,options={}) + from=GeoKit::LatLng.normalize(from) + + units = options[:units] || GeoKit::default_units + + heading=from.heading_to(to) + distance=from.distance_to(to,options) + midpoint=from.endpoint(heading,distance/2,options) + end + + # Geocodes a location using the multi geocoder. + def geocode(location) + res = Geocoders::MultiGeocoder.geocode(location) + return res if res.success + raise GeoKit::Geocoders::GeocodeError + end + + protected + + def deg2rad(degrees) + degrees.to_f / 180.0 * Math::PI + end + + def rad2deg(rad) + rad.to_f * 180.0 / Math::PI + end + + def to_heading(rad) + (rad2deg(rad)+360)%360 + end + + # Returns the multiplier used to obtain the correct distance units. + def units_sphere_multiplier(units) + units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS + end + + # Returns the number of units per latitude degree. + def units_per_latitude_degree(units) + units == :miles ? MILES_PER_LATITUDE_DEGREE : KMS_PER_LATITUDE_DEGREE + end + + # Returns the number units per longitude degree. + def units_per_longitude_degree(lat, units) + miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs + units == :miles ? miles_per_longitude_degree : miles_per_longitude_degree * KMS_PER_MILE + end + end + + # ----------------------------------------------------------------------------------------------- + # Instance methods below here + # ----------------------------------------------------------------------------------------------- + + # Extracts a LatLng instance. Use with models that are acts_as_mappable + def to_lat_lng + return self if instance_of?(GeoKit::LatLng) || instance_of?(GeoKit::GeoLoc) + return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable) + return nil + end + + # Returns the distance from another point. The other point parameter is + # required to have lat and lng attributes. Valid options are: + # :units - valid values are :miles or :kms (:miles is the default) + # :formula - valid values are :flat or :sphere (:sphere is the default) + def distance_to(other, options={}) + self.class.distance_between(self, other, options) + end + alias distance_from distance_to + + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) + # to the given point. The given point can be a LatLng or a string to be Geocoded + def heading_to(other) + self.class.heading_between(self,other) + end + + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) + # FROM the given point. The given point can be a LatLng or a string to be Geocoded + def heading_from(other) + self.class.heading_between(other,self) + end + + # Returns the endpoint, given a heading (in degrees) and distance. + # Valid option: + # :units - valid values are :miles or :kms (:miles is the default) + def endpoint(heading,distance,options={}) + self.class.endpoint(self,heading,distance,options) + end + + # Returns the midpoint, given another point on the map. + # Valid option: + # :units - valid values are :miles or :kms (:miles is the default) + def midpoint_to(other, options={}) + self.class.midpoint_between(self,other,options) + end + + end + + class LatLng + include Mappable + + attr_accessor :lat, :lng + + # Accepts latitude and longitude or instantiates an empty instance + # if lat and lng are not provided. Converted to floats if provided + def initialize(lat=nil, lng=nil) + lat = lat.to_f if lat && !lat.is_a?(Numeric) + lng = lng.to_f if lng && !lng.is_a?(Numeric) + @lat = lat + @lng = lng + end + + # Latitude attribute setter; stored as a float. + def lat=(lat) + @lat = lat.to_f if lat + end + + # Longitude attribute setter; stored as a float; + def lng=(lng) + @lng=lng.to_f if lng + end + + # Returns the lat and lng attributes as a comma-separated string. + def ll + "#{lat},#{lng}" + end + + #returns a string with comma-separated lat,lng values + def to_s + ll + end + + #returns a two-element array + def to_a + [lat,lng] + end + # Returns true if the candidate object is logically equal. Logical equivalence + # is true if the lat and lng attributes are the same for both objects. + def ==(other) + other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false + end + + # A *class* method to take anything which can be inferred as a point and generate + # a LatLng from it. You should use this anything you're not sure what the input is, + # and want to deal with it as a LatLng if at all possible. Can take: + # 1) two arguments (lat,lng) + # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234" + # 3) a string which can be geocoded on the fly + # 4) an array in the format [37.1234,-129.1234] + # 5) a LatLng or GeoLoc (which is just passed through as-is) + # 6) anything which acts_as_mappable -- a LatLng will be extracted from it + def self.normalize(thing,other=nil) + # if an 'other' thing is supplied, normalize the input by creating an array of two elements + thing=[thing,other] if other + + if thing.is_a?(String) + thing.strip! + if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/) + return GeoKit::LatLng.new(match[1],match[2]) + else + res = GeoKit::Geocoders::MultiGeocoder.geocode(thing) + return res if res.success + raise GeoKit::Geocoders::GeocodeError + end + elsif thing.is_a?(Array) && thing.size==2 + return GeoKit::LatLng.new(thing[0],thing[1]) + elsif thing.is_a?(LatLng) # will also be true for GeoLocs + return thing + elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name) + return thing.to_lat_lng + end + + throw ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.") + end + + end + + # This class encapsulates the result of a geocoding call + # It's primary purpose is to homogenize the results of multiple + # geocoding providers. It also provides some additional functionality, such as + # the "full address" method for geocoders that do not provide a + # full address in their results (for example, Yahoo), and the "is_us" method. + class GeoLoc < LatLng + # Location attributes. Full address is a concatenation of all values. For example: + # 100 Spear St, San Francisco, CA, 94101, US + attr_accessor :street_address, :city, :state, :zip, :country_code, :full_address + # Attributes set upon return from geocoding. Success will be true for successful + # geocode lookups. The provider will be set to the name of the providing geocoder. + # Finally, precision is an indicator of the accuracy of the geocoding. + attr_accessor :success, :provider, :precision + # Street number and street name are extracted from the street address attribute. + attr_reader :street_number, :street_name + + # Constructor expects a hash of symbols to correspond with attributes. + def initialize(h={}) + @street_address=h[:street_address] + @city=h[:city] + @state=h[:state] + @zip=h[:zip] + @country_code=h[:country_code] + @success=false + @precision='unknown' + super(h[:lat],h[:lng]) + end + + # Returns true if geocoded to the United States. + def is_us? + country_code == 'US' + end + + # full_address is provided by google but not by yahoo. It is intended that the google + # geocoding method will provide the full address, whereas for yahoo it will be derived + # from the parts of the address we do have. + def full_address + @full_address ? @full_address : to_geocodeable_s + end + + # Extracts the street number from the street address if the street address + # has a value. + def street_number + street_address[/(\d*)/] if street_address + end + + # Returns the street name portion of the street address. + def street_name + street_address[street_number.length, street_address.length].strip if street_address + end + + # gives you all the important fields as key-value pairs + def hash + res={} + [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) } + res + end + alias to_hash hash + + # Sets the city after capitalizing each word within the city name. + def city=(city) + @city = city.titleize if city + end + + # Sets the street address after capitalizing each word within the street address. + def street_address=(address) + @street_address = address.titleize if address + end + + # Returns a comma-delimited string consisting of the street address, city, state, + # zip, and country code. Only includes those attributes that are non-blank. + def to_geocodeable_s + a=[street_address, city, state, zip, country_code].compact + a.delete_if { |e| !e || e == '' } + a.join(', ') + end + + # Returns a string representation of the instance. + def to_s + "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}" + end + end + + # Bounds represents a rectangular bounds, defined by the SW and NE corners + class Bounds + # sw and ne are LatLng objects + attr_accessor :sw, :ne + + # provide sw and ne to instantiate a new Bounds instance + def initialize(sw,ne) + raise ArguementError if !(sw.is_a?(GeoKit::LatLng) && ne.is_a?(GeoKit::LatLng)) + @sw,@ne=sw,ne + end + + #returns the a single point which is the center of the rectangular bounds + def center + @sw.midpoint_to(@ne) + end + + # a simple string representation:sw,ne + def to_s + "#{@sw.to_s},#{@ne.to_s}" + end + + # a two-element array of two-element arrays: sw,ne + def to_a + [@sw.to_a, @ne.to_a] + end + + # Returns true if the bounds contain the passed point. + # allows for bounds which cross the meridian + def contains?(point) + point=GeoKit::LatLng.normalize(point) + res = point.lat > @sw.lat && point.lat < @ne.lat + if crosses_meridian? + res &= point.lng < @ne.lng || point.lng > @sw.lng + else + res &= point.lng < @ne.lng && point.lng > @sw.lng + end + res + end + + # returns true if the bounds crosses the international dateline + def crosses_meridian? + @sw.lng > @ne.lng + end + + # Returns true if the candidate object is logically equal. Logical equivalence + # is true if the lat and lng attributes are the same for both objects. + def ==(other) + other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false + end + + class <true +end + +# Uses deviations from conventions. +class CustomLocation < ActiveRecord::Base #:nodoc: all + belongs_to :company + acts_as_mappable :distance_column_name => 'dist', + :default_units => :kms, + :default_formula => :flat, + :lat_column_name => 'latitude', + :lng_column_name => 'longitude' + + def to_s + "lat: #{latitude} lng: #{longitude} dist: #{dist}" + end +end + +class ActsAsMappableTest < Test::Unit::TestCase #:nodoc: all + + LOCATION_A_IP = "217.10.83.5" + + #self.fixture_path = File.dirname(__FILE__) + '/fixtures' + #self.fixture_path = RAILS_ROOT + '/test/fixtures/' + #puts "Rails Path #{RAILS_ROOT}" + #puts "Fixture Path: #{self.fixture_path}" + #self.fixture_path = ' /Users/bill_eisenhauer/Projects/geokit_test/test/fixtures/' + fixtures :companies, :locations, :custom_locations, :stores + + def setup + @location_a = GeoKit::GeoLoc.new + @location_a.lat = 32.918593 + @location_a.lng = -96.958444 + @location_a.city = "Irving" + @location_a.state = "TX" + @location_a.country_code = "US" + @location_a.success = true + + @sw = GeoKit::LatLng.new(32.91663,-96.982841) + @ne = GeoKit::LatLng.new(32.96302,-96.919495) + @bounds_center=GeoKit::LatLng.new((@sw.lat+@ne.lat)/2,(@sw.lng+@ne.lng)/2) + + @starbucks = companies(:starbucks) + @loc_a = locations(:a) + @custom_loc_a = custom_locations(:a) + @loc_e = locations(:e) + @custom_loc_e = custom_locations(:e) + end + + def test_override_default_units_the_hard_way + Location.default_units = :kms + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 5, locations.size + locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 5, locations + Location.default_units = :miles + end + + def test_include + locations = Location.find(:all, :origin => @loc_a, :include => :company, :conditions => "company_id = 1") + assert !locations.empty? + assert_equal 1, locations[0].company.id + assert_equal 'Starbucks', locations[0].company.name + end + + def test_distance_between_geocoded + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("San Francisco, CA").returns(@location_a) + assert_equal 0, Location.distance_between("Irving, TX", "San Francisco, CA") + end + + def test_distance_to_geocoded + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) + assert_equal 0, @custom_loc_a.distance_to("Irving, TX") + end + + def test_distance_to_geocoded_error + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(GeoKit::GeoLoc.new) + assert_raise(GeoKit::Geocoders::GeocodeError) { @custom_loc_a.distance_to("Irving, TX") } + end + + def test_custom_attributes_distance_calculations + assert_equal 0, @custom_loc_a.distance_to(@loc_a) + assert_equal 0, CustomLocation.distance_between(@custom_loc_a, @loc_a) + end + + def test_distance_column_in_select + locations = Location.find(:all, :origin => @loc_a, :order => "distance ASC") + assert_equal 6, locations.size + assert_equal 0, @loc_a.distance_to(locations.first) + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 + end + + def test_find_with_distance_condition + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 5, locations.size + locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 5, locations + end + + def test_find_with_distance_condition_with_units_override + locations = Location.find(:all, :origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") + assert_equal 5, locations.size + locations = Location.count(:origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") + assert_equal 5, locations + end + + def test_find_with_distance_condition_with_formula_override + locations = Location.find(:all, :origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") + assert_equal 6, locations.size + locations = Location.count(:origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") + assert_equal 6, locations + end + + def test_find_within + locations = Location.find_within(3.97, :origin => @loc_a) + assert_equal 5, locations.size + locations = Location.count_within(3.97, :origin => @loc_a) + assert_equal 5, locations + end + + def test_find_within_with_token + locations = Location.find(:all, :within => 3.97, :origin => @loc_a) + assert_equal 5, locations.size + locations = Location.count(:within => 3.97, :origin => @loc_a) + assert_equal 5, locations + end + + def test_find_within_with_coordinates + locations = Location.find_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) + assert_equal 5, locations.size + locations = Location.count_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) + assert_equal 5, locations + end + + def test_find_with_compound_condition + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations.size + locations = Location.count(:origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations + end + + def test_find_with_secure_compound_condition + locations = Location.find(:all, :origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) + assert_equal 2, locations.size + locations = Location.count(:origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) + assert_equal 2, locations + end + + def test_find_beyond + locations = Location.find_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations.size + locations = Location.count_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations + end + + def test_find_beyond_with_token + locations = Location.find(:all, :beyond => 3.95, :origin => @loc_a) + assert_equal 1, locations.size + locations = Location.count(:beyond => 3.95, :origin => @loc_a) + assert_equal 1, locations + end + + def test_find_beyond_with_coordinates + locations = Location.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 1, locations.size + locations = Location.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 1, locations + end + + def test_find_range_with_token + locations = Location.find(:all, :range => 0..10, :origin => @loc_a) + assert_equal 6, locations.size + locations = Location.count(:range => 0..10, :origin => @loc_a) + assert_equal 6, locations + end + + def test_find_range_with_token_with_conditions + locations = Location.find(:all, :origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) + assert_equal 2, locations.size + locations = Location.count(:origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) + assert_equal 2, locations + end + + def test_find_range_with_token_excluding_end + locations = Location.find(:all, :range => 0...10, :origin => @loc_a) + assert_equal 6, locations.size + locations = Location.count(:range => 0...10, :origin => @loc_a) + assert_equal 6, locations + end + + def test_find_nearest + assert_equal @loc_a, Location.find_nearest(:origin => @loc_a) + end + + def test_find_nearest_through_find + assert_equal @loc_a, Location.find(:nearest, :origin => @loc_a) + end + + def test_find_nearest_with_coordinates + assert_equal @loc_a, Location.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) + end + + def test_find_farthest + assert_equal @loc_e, Location.find_farthest(:origin => @loc_a) + end + + def test_find_farthest_through_find + assert_equal @loc_e, Location.find(:farthest, :origin => @loc_a) + end + + def test_find_farthest_with_coordinates + assert_equal @loc_e, Location.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) + end + + def test_scoped_distance_column_in_select + locations = @starbucks.locations.find(:all, :origin => @loc_a, :order => "distance ASC") + assert_equal 5, locations.size + assert_equal 0, @loc_a.distance_to(locations.first) + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 + end + + def test_scoped_find_with_distance_condition + locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 4, locations.size + locations = @starbucks.locations.count(:origin => @loc_a, :conditions => "distance < 3.97") + assert_equal 4, locations + end + + def test_scoped_find_within + locations = @starbucks.locations.find_within(3.97, :origin => @loc_a) + assert_equal 4, locations.size + locations = @starbucks.locations.count_within(3.97, :origin => @loc_a) + assert_equal 4, locations + end + + def test_scoped_find_with_compound_condition + locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations.size + locations = @starbucks.locations.count( :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations + end + + def test_scoped_find_beyond + locations = @starbucks.locations.find_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations.size + locations = @starbucks.locations.count_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations + end + + def test_scoped_find_nearest + assert_equal @loc_a, @starbucks.locations.find_nearest(:origin => @loc_a) + end + + def test_scoped_find_farthest + assert_equal @loc_e, @starbucks.locations.find_farthest(:origin => @loc_a) + end + + def test_ip_geocoded_distance_column_in_select + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find(:all, :origin => LOCATION_A_IP, :order => "distance ASC") + assert_equal 6, locations.size + assert_equal 0, @loc_a.distance_to(locations.first) + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 + end + + def test_ip_geocoded_find_with_distance_condition + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 3.97") + assert_equal 5, locations.size + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 3.97") + assert_equal 5, locations + end + + def test_ip_geocoded_find_within + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find_within(3.97, :origin => LOCATION_A_IP) + assert_equal 5, locations.size + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.count_within(3.97, :origin => LOCATION_A_IP) + assert_equal 5, locations + end + + def test_ip_geocoded_find_with_compound_condition + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations.size + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") + assert_equal 2, locations + end + + def test_ip_geocoded_find_with_secure_compound_condition + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) + assert_equal 2, locations.size + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.count(:origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) + assert_equal 2, locations + end + + def test_ip_geocoded_find_beyond + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.find_beyond(3.95, :origin => LOCATION_A_IP) + assert_equal 1, locations.size + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + locations = Location.count_beyond(3.95, :origin => LOCATION_A_IP) + assert_equal 1, locations + end + + def test_ip_geocoded_find_nearest + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + assert_equal @loc_a, Location.find_nearest(:origin => LOCATION_A_IP) + end + + def test_ip_geocoded_find_farthest + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) + assert_equal @loc_e, Location.find_farthest(:origin => LOCATION_A_IP) + end + + def test_ip_geocoder_exception + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with('127.0.0.1').returns(GeoKit::GeoLoc.new) + assert_raises GeoKit::Geocoders::GeocodeError do + Location.find_farthest(:origin => '127.0.0.1') + end + end + + def test_address_geocode + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with('Irving, TX').returns(@location_a) + locations = Location.find(:all, :origin => 'Irving, TX', :conditions => ["distance < ? and city = ?", 5, 'Coppell']) + assert_equal 2, locations.size + end + + def test_find_with_custom_distance_condition + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 3.97") + assert_equal 5, locations.size + locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 3.97") + assert_equal 5, locations + end + + def test_find_with_custom_distance_condition_using_custom_origin + locations = CustomLocation.find(:all, :origin => @custom_loc_a, :conditions => "dist < 3.97") + assert_equal 5, locations.size + locations = CustomLocation.count(:origin => @custom_loc_a, :conditions => "dist < 3.97") + assert_equal 5, locations + end + + def test_find_within_with_custom + locations = CustomLocation.find_within(3.97, :origin => @loc_a) + assert_equal 5, locations.size + locations = CustomLocation.count_within(3.97, :origin => @loc_a) + assert_equal 5, locations + end + + def test_find_within_with_coordinates_with_custom + locations = CustomLocation.find_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 5, locations.size + locations = CustomLocation.count_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 5, locations + end + + def test_find_with_compound_condition_with_custom + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") + assert_equal 1, locations.size + locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") + assert_equal 1, locations + end + + def test_find_with_secure_compound_condition_with_custom + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) + assert_equal 1, locations.size + locations = CustomLocation.count(:origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) + assert_equal 1, locations + end + + def test_find_beyond_with_custom + locations = CustomLocation.find_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations.size + locations = CustomLocation.count_beyond(3.95, :origin => @loc_a) + assert_equal 1, locations + end + + def test_find_beyond_with_coordinates_with_custom + locations = CustomLocation.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 1, locations.size + locations = CustomLocation.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) + assert_equal 1, locations + end + + def test_find_nearest_with_custom + assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin => @loc_a) + end + + def test_find_nearest_with_coordinates_with_custom + assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) + end + + def test_find_farthest_with_custom + assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin => @loc_a) + end + + def test_find_farthest_with_coordinates_with_custom + assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) + end + + def test_find_with_array_origin + locations = Location.find(:all, :origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") + assert_equal 5, locations.size + locations = Location.count(:origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") + assert_equal 5, locations + end + + + # Bounding box tests + + def test_find_within_bounds + locations = Location.find_within_bounds([@sw,@ne]) + assert_equal 2, locations.size + locations = Location.count_within_bounds([@sw,@ne]) + assert_equal 2, locations + end + + def test_find_within_bounds_ordered_by_distance + locations = Location.find_within_bounds([@sw,@ne], :origin=>@bounds_center, :order=>'distance asc') + assert_equal locations[0], locations(:d) + assert_equal locations[1], locations(:a) + end + + def test_find_within_bounds_with_token + locations = Location.find(:all, :bounds=>[@sw,@ne]) + assert_equal 2, locations.size + locations = Location.count(:bounds=>[@sw,@ne]) + assert_equal 2, locations + end + + def test_find_within_bounds_with_string_conditions + locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>"id !=#{locations(:a).id}") + assert_equal 1, locations.size + end + + def test_find_within_bounds_with_array_conditions + locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>["id != ?", locations(:a).id]) + assert_equal 1, locations.size + end + + def test_auto_geocode + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) + store=Store.new(:address=>'Irving, TX') + store.save + assert_equal store.lat,@location_a.lat + assert_equal store.lng,@location_a.lng + assert_equal 0, store.errors.size + end + + def test_auto_geocode_failure + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("BOGUS").returns(GeoKit::GeoLoc.new) + store=Store.new(:address=>'BOGUS') + store.save + assert store.new_record? + assert_equal 1, store.errors.size + end +end diff --git a/vendor/plugins/geokit/test/base_geocoder_test.rb b/vendor/plugins/geokit/test/base_geocoder_test.rb new file mode 100644 index 0000000..b51a739 --- /dev/null +++ b/vendor/plugins/geokit/test/base_geocoder_test.rb @@ -0,0 +1,57 @@ +require 'test/unit' +require 'net/http' +require 'rubygems' +require 'mocha' +require File.join(File.dirname(__FILE__), '../../../../config/environment') + + +class MockSuccess < Net::HTTPSuccess #:nodoc: all + def initialize + end +end + +class MockFailure < Net::HTTPServiceUnavailable #:nodoc: all + def initialize + end +end + +# Base class for testing geocoders. +class BaseGeocoderTest < Test::Unit::TestCase #:nodoc: all + + # Defines common test fixtures. + def setup + @address = 'San Francisco, CA' + @full_address = '100 Spear St, San Francisco, CA, 94105-1522, US' + @full_address_short_zip = '100 Spear St, San Francisco, CA, 94105, US' + + @success = GeoKit::GeoLoc.new({:city=>"SAN FRANCISCO", :state=>"CA", :country_code=>"US", :lat=>37.7742, :lng=>-122.417068}) + @success.success = true + end + + def test_timeout_call_web_service + GeoKit::Geocoders::Geocoder.class_eval do + def self.do_get(url) + sleep(2) + end + end + url = "http://www.anything.com" + GeoKit::Geocoders::timeout = 1 + assert_nil GeoKit::Geocoders::Geocoder.call_geocoder_service(url) + end + + def test_successful_call_web_service + url = "http://www.anything.com" + GeoKit::Geocoders::Geocoder.expects(:do_get).with(url).returns("SUCCESS") + assert_equal "SUCCESS", GeoKit::Geocoders::Geocoder.call_geocoder_service(url) + end + + def test_find_geocoder_methods + public_methods = GeoKit::Geocoders::Geocoder.public_methods + assert public_methods.include?("yahoo_geocoder") + assert public_methods.include?("google_geocoder") + assert public_methods.include?("ca_geocoder") + assert public_methods.include?("us_geocoder") + assert public_methods.include?("multi_geocoder") + assert public_methods.include?("ip_geocoder") + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/bounds_test.rb b/vendor/plugins/geokit/test/bounds_test.rb new file mode 100644 index 0000000..4cabfde --- /dev/null +++ b/vendor/plugins/geokit/test/bounds_test.rb @@ -0,0 +1,75 @@ +$LOAD_PATH.unshift File.join('..', 'lib') +require 'geo_kit/mappable' +require 'test/unit' + +class BoundsTest < Test::Unit::TestCase #:nodoc: all + + def setup + # This is the area in Texas + @sw = GeoKit::LatLng.new(32.91663,-96.982841) + @ne = GeoKit::LatLng.new(32.96302,-96.919495) + @bounds=GeoKit::Bounds.new(@sw,@ne) + @loc_a=GeoKit::LatLng.new(32.918593,-96.958444) # inside bounds + @loc_b=GeoKit::LatLng.new(32.914144,-96.958444) # outside bouds + + # this is a cross-meridan area + @cross_meridian=GeoKit::Bounds.normalize([30,170],[40,-170]) + @inside_cm=GeoKit::LatLng.new(35,175) + @inside_cm_2=GeoKit::LatLng.new(35,-175) + @east_of_cm=GeoKit::LatLng.new(35,-165) + @west_of_cm=GeoKit::LatLng.new(35,165) + + end + + def test_equality + assert_equal GeoKit::Bounds.new(@sw,@ne), GeoKit::Bounds.new(@sw,@ne) + end + + def test_normalize + res=GeoKit::Bounds.normalize(@sw,@ne) + assert_equal res,GeoKit::Bounds.new(@sw,@ne) + res=GeoKit::Bounds.normalize([@sw,@ne]) + assert_equal res,GeoKit::Bounds.new(@sw,@ne) + res=GeoKit::Bounds.normalize([@sw.lat,@sw.lng],[@ne.lat,@ne.lng]) + assert_equal res,GeoKit::Bounds.new(@sw,@ne) + res=GeoKit::Bounds.normalize([[@sw.lat,@sw.lng],[@ne.lat,@ne.lng]]) + assert_equal res,GeoKit::Bounds.new(@sw,@ne) + end + + def test_point_inside_bounds + assert @bounds.contains?(@loc_a) + end + + def test_point_outside_bounds + assert !@bounds.contains?(@loc_b) + end + + def test_point_inside_bounds_cross_meridian + assert @cross_meridian.contains?(@inside_cm) + assert @cross_meridian.contains?(@inside_cm_2) + end + + def test_point_outside_bounds_cross_meridian + assert !@cross_meridian.contains?(@east_of_cm) + assert !@cross_meridian.contains?(@west_of_cm) + end + + def test_center + assert_in_delta 32.939828,@bounds.center.lat,0.00005 + assert_in_delta -96.9511763,@bounds.center.lng,0.00005 + end + + def test_center_cross_meridian + assert_in_delta 35.41160, @cross_meridian.center.lat,0.00005 + assert_in_delta 179.38112, @cross_meridian.center.lng,0.00005 + end + + def test_creation_from_circle + bounds=GeoKit::Bounds.from_point_and_radius([32.939829, -96.951176],2.5) + inside=GeoKit::LatLng.new 32.9695270000,-96.9901590000 + outside=GeoKit::LatLng.new 32.8951550000,-96.9584440000 + assert bounds.contains?(inside) + assert !bounds.contains?(outside) + end + +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/ca_geocoder_test.rb b/vendor/plugins/geokit/test/ca_geocoder_test.rb new file mode 100644 index 0000000..9d797ce --- /dev/null +++ b/vendor/plugins/geokit/test/ca_geocoder_test.rb @@ -0,0 +1,41 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +GeoKit::Geocoders::geocoder_ca = "SOMEKEYVALUE" + +class CaGeocoderTest < BaseGeocoderTest #:nodoc: all + + CA_SUCCESS=<<-EOF + + 49.243086-123.153684 + EOF + + def setup + @ca_full_hash = {:street_address=>"2105 West 32nd Avenue",:city=>"Vancouver", :state=>"BC"} + @ca_full_loc = GeoKit::GeoLoc.new(@ca_full_hash) + end + + def test_geocoder_with_geo_loc_with_account + response = MockSuccess.new + response.expects(:body).returns(CA_SUCCESS) + url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" + GeoKit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) + verify(GeoKit::Geocoders::CaGeocoder.geocode(@ca_full_loc)) + end + + def test_service_unavailable + response = MockFailure.new + #Net::HTTP.expects(:get_response).with(URI.parse("http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml")).returns(response) + url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" + GeoKit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) + assert !GeoKit::Geocoders::CaGeocoder.geocode(@ca_full_loc).success + end + + private + + def verify(location) + assert_equal "BC", location.state + assert_equal "Vancouver", location.city + assert_equal "49.243086,-123.153684", location.ll + assert !location.is_us? + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/fixtures/companies.yml b/vendor/plugins/geokit/test/fixtures/companies.yml new file mode 100644 index 0000000..d48a2e8 --- /dev/null +++ b/vendor/plugins/geokit/test/fixtures/companies.yml @@ -0,0 +1,7 @@ +starbucks: + id: 1 + name: Starbucks + +barnes_and_noble: + id: 2 + name: Barnes & Noble \ No newline at end of file diff --git a/vendor/plugins/geokit/test/fixtures/custom_locations.yml b/vendor/plugins/geokit/test/fixtures/custom_locations.yml new file mode 100644 index 0000000..f5a1e92 --- /dev/null +++ b/vendor/plugins/geokit/test/fixtures/custom_locations.yml @@ -0,0 +1,54 @@ +a: + id: 1 + company_id: 1 + street: 7979 N MacArthur Blvd + city: Irving + state: TX + postal_code: 75063 + latitude: 32.918593 + longitude: -96.958444 +b: + id: 2 + company_id: 1 + street: 7750 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75063 + latitude: 32.914144 + longitude: -96.958444 +c: + id: 3 + company_id: 1 + street: 5904 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75039 + latitude: 32.895155 + longitude: -96.958444 +d: + id: 4 + company_id: 1 + street: 817 S Macarthur Blvd # 145 + city: Coppell + state: TX + postal_code: 75019 + latitude: 32.951613 + longitude: -96.958444 +e: + id: 5 + company_id: 1 + street: 106 N Denton Tap Rd # 350 + city: Coppell + state: TX + postal_code: 75019 + latitude: 32.969527 + longitude: -96.990159 +f: + id: 6 + company_id: 2 + street: 5904 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75039 + latitude: 32.895155 + longitude: -96.958444 \ No newline at end of file diff --git a/vendor/plugins/geokit/test/fixtures/locations.yml b/vendor/plugins/geokit/test/fixtures/locations.yml new file mode 100644 index 0000000..779faba --- /dev/null +++ b/vendor/plugins/geokit/test/fixtures/locations.yml @@ -0,0 +1,54 @@ +a: + id: 1 + company_id: 1 + street: 7979 N MacArthur Blvd + city: Irving + state: TX + postal_code: 75063 + lat: 32.918593 + lng: -96.958444 +b: + id: 2 + company_id: 1 + street: 7750 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75063 + lat: 32.914144 + lng: -96.958444 +c: + id: 3 + company_id: 1 + street: 5904 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75039 + lat: 32.895155 + lng: -96.958444 +d: + id: 4 + company_id: 1 + street: 817 S Macarthur Blvd # 145 + city: Coppell + state: TX + postal_code: 75019 + lat: 32.951613 + lng: -96.958444 +e: + id: 5 + company_id: 1 + street: 106 N Denton Tap Rd # 350 + city: Coppell + state: TX + postal_code: 75019 + lat: 32.969527 + lng: -96.990159 +f: + id: 6 + company_id: 2 + street: 5904 N Macarthur Blvd # 160 + city: Irving + state: TX + postal_code: 75039 + lat: 32.895155 + lng: -96.958444 \ No newline at end of file diff --git a/vendor/plugins/geokit/test/fixtures/stores.yml b/vendor/plugins/geokit/test/fixtures/stores.yml new file mode 100644 index 0000000..e69de29 diff --git a/vendor/plugins/geokit/test/geoloc_test.rb b/vendor/plugins/geokit/test/geoloc_test.rb new file mode 100644 index 0000000..5cf6411 --- /dev/null +++ b/vendor/plugins/geokit/test/geoloc_test.rb @@ -0,0 +1,49 @@ +require 'test/unit' +require File.join(File.dirname(__FILE__), '../../../../config/environment') + +class GeoLocTest < Test::Unit::TestCase #:nodoc: all + + def setup + @loc = GeoKit::GeoLoc.new + end + + def test_is_us + assert !@loc.is_us? + @loc.country_code = 'US' + assert @loc.is_us? + end + + def test_street_number + @loc.street_address = '123 Spear St.' + assert_equal '123', @loc.street_number + end + + def test_street_name + @loc.street_address = '123 Spear St.' + assert_equal 'Spear St.', @loc.street_name + end + + def test_city + @loc.city = "san francisco" + assert_equal 'San Francisco', @loc.city + end + + def test_full_address + @loc.city = 'San Francisco' + @loc.state = 'CA' + @loc.zip = '94105' + @loc.country_code = 'US' + assert_equal 'San Francisco, CA, 94105, US', @loc.full_address + @loc.full_address = 'Irving, TX, 75063, US' + assert_equal 'Irving, TX, 75063, US', @loc.full_address + end + + def test_hash + @loc.city = 'San Francisco' + @loc.state = 'CA' + @loc.zip = '94105' + @loc.country_code = 'US' + @another = GeoKit::GeoLoc.new @loc.to_hash + assert_equal @loc, @another + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/google_geocoder_test.rb b/vendor/plugins/geokit/test/google_geocoder_test.rb new file mode 100644 index 0000000..dfa8a0c --- /dev/null +++ b/vendor/plugins/geokit/test/google_geocoder_test.rb @@ -0,0 +1,88 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +GeoKit::Geocoders::google = 'Google' + +class GoogleGeocoderTest < BaseGeocoderTest #:nodoc: all + + GOOGLE_FULL=<<-EOF.strip + 100 spear st, san francisco, ca200geocode

100 Spear St, San Francisco, CA 94105, USA
USCASan FranciscoSan Francisco100 Spear St94105-122.393985,37.792501,0 + EOF + + GOOGLE_CITY=<<-EOF.strip + San Francisco200geocode
San Francisco, CA, USA
USCASan Francisco-122.418333,37.775000,0
+ EOF + + def setup + super + @google_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105", :country_code=>"US"} + @google_city_hash = {:city=>"San Francisco", :state=>"CA"} + + @google_full_loc = GeoKit::GeoLoc.new(@google_full_hash) + @google_city_loc = GeoKit::GeoLoc.new(@google_city_hash) + end + + def test_google_full_address + response = MockSuccess.new + response.expects(:body).returns(GOOGLE_FULL) + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@address) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo + assert res.is_us? + assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo + assert_equal "google", res.provider + end + + def test_google_full_address_with_geo_loc + response = MockSuccess.new + response.expects(:body).returns(GOOGLE_FULL) + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@full_address_short_zip)}&output=xml&key=Google&oe=utf-8" + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@google_full_loc) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo + assert res.is_us? + assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo + assert_equal "google", res.provider + end + + def test_google_city + response = MockSuccess.new + response.expects(:body).returns(GOOGLE_CITY) + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@address) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.775,-122.418333", res.ll + assert res.is_us? + assert_equal "San Francisco, CA, USA", res.full_address + assert_nil res.street_address + assert_equal "google", res.provider + end + + def test_google_city_with_geo_loc + response = MockSuccess.new + response.expects(:body).returns(GOOGLE_CITY) + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@google_city_loc) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.775,-122.418333", res.ll + assert res.is_us? + assert_equal "San Francisco, CA, USA", res.full_address + assert_nil res.street_address + assert_equal "google", res.provider + end + + def test_service_unavailable + response = MockFailure.new + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) + assert !GeoKit::Geocoders::GoogleGeocoder.geocode(@google_city_loc).success + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/ip_geocode_lookup_test.rb b/vendor/plugins/geokit/test/ip_geocode_lookup_test.rb new file mode 100644 index 0000000..31f3582 --- /dev/null +++ b/vendor/plugins/geokit/test/ip_geocode_lookup_test.rb @@ -0,0 +1,84 @@ +require File.join(File.dirname(__FILE__), '../../../../config/environment') +require 'action_controller/test_process' +require 'test/unit' +require 'rubygems' +require 'mocha' + + +class LocationAwareController < ActionController::Base #:nodoc: all + geocode_ip_address + + def index + render :nothing => true + end +end + +class ActionController::TestRequest #:nodoc: all + attr_accessor :remote_ip +end + +# Re-raise errors caught by the controller. +class LocationAwareController #:nodoc: all + def rescue_action(e) raise e end; +end + +class IpGeocodeLookupTest < Test::Unit::TestCase #:nodoc: all + + def setup + @success = GeoKit::GeoLoc.new + @success.provider = "hostip" + @success.lat = 41.7696 + @success.lng = -88.4588 + @success.city = "Sugar Grove" + @success.state = "IL" + @success.country_code = "US" + @success.success = true + + @failure = GeoKit::GeoLoc.new + @failure.provider = "hostip" + @failure.city = "(Private Address)" + @failure.success = false + + @controller = LocationAwareController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_no_location_in_cookie_or_session + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with("good ip").returns(@success) + @request.remote_ip = "good ip" + get :index + verify + end + + def test_location_in_cookie + @request.remote_ip = "good ip" + @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) + get :index + verify + end + + def test_location_in_session + @request.remote_ip = "good ip" + @request.session[:geo_location] = @success + @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) + get :index + verify + end + + def test_ip_not_located + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with("bad ip").returns(@failure) + @request.remote_ip = "bad ip" + get :index + assert_nil @request.session[:geo_location] + end + + private + + def verify + assert_response :success + assert_equal @success, @request.session[:geo_location] + assert_not_nil cookies['geo_location'] + assert_equal @success, YAML.load(cookies['geo_location'].join) + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/ipgeocoder_test.rb b/vendor/plugins/geokit/test/ipgeocoder_test.rb new file mode 100644 index 0000000..4d2b6a0 --- /dev/null +++ b/vendor/plugins/geokit/test/ipgeocoder_test.rb @@ -0,0 +1,87 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +class IpGeocoderTest < BaseGeocoderTest #:nodoc: all + + IP_FAILURE=<<-EOF + Country: (Private Address) (XX) + City: (Private Address) + Latitude: + Longitude: + EOF + + IP_SUCCESS=<<-EOF + Country: UNITED STATES (US) + City: Sugar Grove, IL + Latitude: 41.7696 + Longitude: -88.4588 + EOF + + IP_UNICODED=<<-EOF + Country: SWEDEN (SE) + City: Borås + Latitude: 57.7167 + Longitude: 12.9167 + EOF + + def setup + super + @success.provider = "hostip" + end + + def test_successful_lookup + success = MockSuccess.new + success.expects(:body).returns(IP_SUCCESS) + url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) + location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') + assert_not_nil location + assert_equal 41.7696, location.lat + assert_equal -88.4588, location.lng + assert_equal "Sugar Grove", location.city + assert_equal "IL", location.state + assert_equal "US", location.country_code + assert_equal "hostip", location.provider + assert location.success + end + + def test_unicoded_lookup + success = MockSuccess.new + success.expects(:body).returns(IP_UNICODED) + url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) + location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') + assert_not_nil location + assert_equal 57.7167, location.lat + assert_equal 12.9167, location.lng + assert_equal "Borås", location.city + assert_nil location.state + assert_equal "SE", location.country_code + assert_equal "hostip", location.provider + assert location.success + end + + def test_failed_lookup + failure = MockSuccess.new + failure.expects(:body).returns(IP_FAILURE) + url = 'http://api.hostip.info/get_html.php?ip=0.0.0.0&position=true' + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) + location = GeoKit::Geocoders::IpGeocoder.geocode("0.0.0.0") + assert_not_nil location + assert !location.success + end + + def test_invalid_ip + location = GeoKit::Geocoders::IpGeocoder.geocode("blah") + assert_not_nil location + assert !location.success + end + + def test_service_unavailable + failure = MockFailure.new + url = 'http://api.hostip.info/get_html.php?ip=0.0.0.0&position=true' + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) + location = GeoKit::Geocoders::IpGeocoder.geocode("0.0.0.0") + assert_not_nil location + assert !location.success + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/latlng_test.rb b/vendor/plugins/geokit/test/latlng_test.rb new file mode 100644 index 0000000..993d2be --- /dev/null +++ b/vendor/plugins/geokit/test/latlng_test.rb @@ -0,0 +1,112 @@ +$LOAD_PATH.unshift File.join('..', 'lib') +require 'geo_kit/mappable' +require 'test/unit' + +class LatLngTest < Test::Unit::TestCase #:nodoc: all + + def setup + @loc_a = GeoKit::LatLng.new(32.918593,-96.958444) + @loc_e = GeoKit::LatLng.new(32.969527,-96.990159) + @point = GeoKit::LatLng.new(@loc_a.lat, @loc_a.lng) + end + + def test_distance_between_same_using_defaults + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a) + assert_equal 0, @loc_a.distance_to(@loc_a) + end + + def test_distance_between_same_with_miles_and_flat + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :flat) + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :flat) + end + + def test_distance_between_same_with_kms_and_flat + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :flat) + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :flat) + end + + def test_distance_between_same_with_miles_and_sphere + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :sphere) + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :sphere) + end + + def test_distance_between_same_with_kms_and_sphere + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :sphere) + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :sphere) + end + + def test_distance_between_diff_using_defaults + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e), 0.01 + assert_in_delta 3.97, @loc_a.distance_to(@loc_e), 0.01 + end + + def test_distance_between_diff_with_miles_and_flat + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :flat), 0.2 + assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 + end + + def test_distance_between_diff_with_kms_and_flat + assert_in_delta 6.39, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :flat), 0.4 + assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 + end + + def test_distance_between_diff_with_miles_and_sphere + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :sphere), 0.01 + assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :sphere), 0.01 + end + + def test_distance_between_diff_with_kms_and_sphere + assert_in_delta 6.39, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :sphere), 0.01 + assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :sphere), 0.01 + end + + def test_manually_mixed_in + assert_equal 0, GeoKit::LatLng.distance_between(@point, @point) + assert_equal 0, @point.distance_to(@point) + assert_equal 0, @point.distance_to(@loc_a) + assert_in_delta 3.97, @point.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 + assert_in_delta 6.39, @point.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 + end + + def test_heading_between + assert_in_delta 332, GeoKit::LatLng.heading_between(@loc_a,@loc_e), 0.5 + end + + def test_heading_to + assert_in_delta 332, @loc_a.heading_to(@loc_e), 0.5 + end + + def test_class_endpoint + endpoint=GeoKit::LatLng.endpoint(@loc_a, 332, 3.97) + assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 + assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 + end + + def test_instance_endpoint + endpoint=@loc_a.endpoint(332, 3.97) + assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 + assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 + end + + def test_midpoint + midpoint=@loc_a.midpoint_to(@loc_e) + assert_in_delta 32.944061, midpoint.lat, 0.0005 + assert_in_delta -96.974296, midpoint.lng, 0.0005 + end + + def test_normalize + lat=37.7690 + lng=-122.443 + res=GeoKit::LatLng.normalize(lat,lng) + assert_equal res,GeoKit::LatLng.new(lat,lng) + res=GeoKit::LatLng.normalize("#{lat}, #{lng}") + assert_equal res,GeoKit::LatLng.new(lat,lng) + res=GeoKit::LatLng.normalize("#{lat} #{lng}") + assert_equal res,GeoKit::LatLng.new(lat,lng) + res=GeoKit::LatLng.normalize("#{lat.to_i} #{lng.to_i}") + assert_equal res,GeoKit::LatLng.new(lat.to_i,lng.to_i) + res=GeoKit::LatLng.normalize([lat,lng]) + assert_equal res,GeoKit::LatLng.new(lat,lng) + end + +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/multi_geocoder_test.rb b/vendor/plugins/geokit/test/multi_geocoder_test.rb new file mode 100644 index 0000000..57f326c --- /dev/null +++ b/vendor/plugins/geokit/test/multi_geocoder_test.rb @@ -0,0 +1,44 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +GeoKit::Geocoders::provider_order=[:google,:yahoo,:us] + +class MultiGeocoderTest < BaseGeocoderTest #:nodoc: all + + def setup + super + @failure = GeoKit::GeoLoc.new + end + + def test_successful_first + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@success) + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) + end + + def test_failover + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@success) + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) + end + + def test_double_failover + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@failure) + GeoKit::Geocoders::UsGeocoder.expects(:geocode).with(@address).returns(@success) + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) + end + + def test_failure + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@failure) + GeoKit::Geocoders::UsGeocoder.expects(:geocode).with(@address).returns(@failure) + assert_equal @failure, GeoKit::Geocoders::MultiGeocoder.geocode(@address) + end + + def test_invalid_provider + temp = GeoKit::Geocoders::provider_order + GeoKit::Geocoders.provider_order = [:bogus] + assert_equal @failure, GeoKit::Geocoders::MultiGeocoder.geocode(@address) + GeoKit::Geocoders.provider_order = temp + end + +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/schema.rb b/vendor/plugins/geokit/test/schema.rb new file mode 100644 index 0000000..ad69e08 --- /dev/null +++ b/vendor/plugins/geokit/test/schema.rb @@ -0,0 +1,31 @@ +ActiveRecord::Schema.define(:version => 0) do + create_table :companies, :force => true do |t| + t.column :name, :string + end + + create_table :locations, :force => true do |t| + t.column :company_id, :integer, :default => 0, :null => false + t.column :street, :string, :limit => 60 + t.column :city, :string, :limit => 60 + t.column :state, :string, :limit => 2 + t.column :postal_code, :string, :limit => 16 + t.column :lat, :decimal, :precision => 15, :scale => 10 + t.column :lng, :decimal, :precision => 15, :scale => 10 + end + + create_table :custom_locations, :force => true do |t| + t.column :company_id, :integer, :default => 0, :null => false + t.column :street, :string, :limit => 60 + t.column :city, :string, :limit => 60 + t.column :state, :string, :limit => 2 + t.column :postal_code, :string, :limit => 16 + t.column :latitude, :decimal, :precision => 15, :scale => 10 + t.column :longitude, :decimal, :precision => 15, :scale => 10 + end + + create_table :stores, :force=> true do |t| + t.column :address, :string + t.column :lat, :decimal, :precision => 15, :scale => 10 + t.column :lng, :decimal, :precision => 15, :scale => 10 + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/test_helper.rb b/vendor/plugins/geokit/test/test_helper.rb new file mode 100644 index 0000000..5cd8f92 --- /dev/null +++ b/vendor/plugins/geokit/test/test_helper.rb @@ -0,0 +1,18 @@ +require 'test/unit' + +plugin_test_dir = File.dirname(__FILE__) + +# Load the Rails environment +require File.join(plugin_test_dir, '../../../../config/environment') +require 'active_record/fixtures' +databases = YAML::load(IO.read(plugin_test_dir + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log") + +# A specific database can be used by setting the DB environment variable +ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'mysql']) + +# Load the test schema into the database +load(File.join(plugin_test_dir, 'schema.rb')) + +# Load fixtures from the plugin +Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/') \ No newline at end of file diff --git a/vendor/plugins/geokit/test/us_geocoder_test.rb b/vendor/plugins/geokit/test/us_geocoder_test.rb new file mode 100644 index 0000000..4457803 --- /dev/null +++ b/vendor/plugins/geokit/test/us_geocoder_test.rb @@ -0,0 +1,47 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +GeoKit::Geocoders::geocoder_us = nil + +class UsGeocoderTest < BaseGeocoderTest #:nodoc: all + + GEOCODER_US_FULL='37.792528,-122.393981,100 Spear St,San Francisco,CA,94105' + + def setup + super + @us_full_hash = {:city=>"San Francisco", :state=>"CA"} + @us_full_loc = GeoKit::GeoLoc.new(@us_full_hash) + end + + def test_geocoder_us + response = MockSuccess.new + response.expects(:body).returns(GEOCODER_US_FULL) + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) + verify(GeoKit::Geocoders::UsGeocoder.geocode(@address)) + end + + def test_geocoder_with_geo_loc + response = MockSuccess.new + response.expects(:body).returns(GEOCODER_US_FULL) + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) + verify(GeoKit::Geocoders::UsGeocoder.geocode(@us_full_loc)) + end + + def test_service_unavailable + response = MockFailure.new + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) + assert !GeoKit::Geocoders::UsGeocoder.geocode(@us_full_loc).success + end + + private + + def verify(location) + assert_equal "CA", location.state + assert_equal "San Francisco", location.city + assert_equal "37.792528,-122.393981", location.ll + assert location.is_us? + assert_equal "100 Spear St, San Francisco, CA, 94105, US", location.full_address #slightly different from yahoo + end +end \ No newline at end of file diff --git a/vendor/plugins/geokit/test/yahoo_geocoder_test.rb b/vendor/plugins/geokit/test/yahoo_geocoder_test.rb new file mode 100644 index 0000000..abf9a4e --- /dev/null +++ b/vendor/plugins/geokit/test/yahoo_geocoder_test.rb @@ -0,0 +1,87 @@ +require File.join(File.dirname(__FILE__), 'base_geocoder_test') + +GeoKit::Geocoders::yahoo = 'Yahoo' + +class YahooGeocoderTest < BaseGeocoderTest #:nodoc: all + YAHOO_FULL=<<-EOF.strip + + 37.792406-122.39411
100 SPEAR ST
SAN FRANCISCOCA94105-1522US
+ + EOF + + YAHOO_CITY=<<-EOF.strip + + 37.7742-122.417068
SAN FRANCISCOCAUS
+ + EOF + + def setup + super + @yahoo_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105-1522", :country_code=>"US"} + @yahoo_city_hash = {:city=>"San Francisco", :state=>"CA"} + @yahoo_full_loc = GeoKit::GeoLoc.new(@yahoo_full_hash) + @yahoo_city_loc = GeoKit::GeoLoc.new(@yahoo_city_hash) + end + + # the testing methods themselves + def test_yahoo_full_address + response = MockSuccess.new + response.expects(:body).returns(YAHOO_FULL) + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) + do_full_address_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@address)) + end + + def test_yahoo_full_address_with_geo_loc + response = MockSuccess.new + response.expects(:body).returns(YAHOO_FULL) + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@full_address)}" + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) + do_full_address_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_full_loc)) + end + + def test_yahoo_city + response = MockSuccess.new + response.expects(:body).returns(YAHOO_CITY) + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) + do_city_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@address)) + end + + def test_yahoo_city_with_geo_loc + response = MockSuccess.new + response.expects(:body).returns(YAHOO_CITY) + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) + do_city_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc)) + end + + def test_service_unavailable + response = MockFailure.new + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) + assert !GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc).success + end + + private + + # next two methods do the assertions for both address-level and city-level lookups + def do_full_address_assertions(res) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.792406,-122.39411", res.ll + assert res.is_us? + assert_equal "100 Spear St, San Francisco, CA, 94105-1522, US", res.full_address + assert_equal "yahoo", res.provider + end + + def do_city_assertions(res) + assert_equal "CA", res.state + assert_equal "San Francisco", res.city + assert_equal "37.7742,-122.417068", res.ll + assert res.is_us? + assert_equal "San Francisco, CA, US", res.full_address + assert_nil res.street_address + assert_equal "yahoo", res.provider + end +end \ No newline at end of file diff --git a/vendor/plugins/ym4r_gm/README b/vendor/plugins/ym4r_gm/README new file mode 100644 index 0000000..b1f0248 --- /dev/null +++ b/vendor/plugins/ym4r_gm/README @@ -0,0 +1,391 @@ +=YM4R/GM plugin for Rails + +This is the latest version of the YM4R/GM plugin for Rails (YM4RGMP4R?). Its aim is to facilitate the use of Google Maps from Rails application. It includes and enhances all the web application-related functionalities found in the YM4R gem as of version 0.4.1. + +==Getting Started +I present here the most common operations you would want to do with YM4R/GM. Read the rest of the documents if you want more details. + +In your controller, here is a typical initialization sequence in action +index+: + def index + @map = GMap.new("map_div") + @map.control_init(:large_map => true,:map_type => true) + @map.center_zoom_init([75.5,-42.56],4) + @map.overlay_init(GMarker.new([75.6,-42.467],:title => "Hello", :info_window => "Info! Info!")) + end + +Here I create a GMap (which will be placed inside a DIV of id +map_div+), add controls (large zoom slider and pan cross + map type selector), set the center and the zoom and add a marker. Of these 4 steps only the creation of the map and the setting of the center and the zoom are absolutely necessary. + +In your view, here is what you would have: + Test + <%= GMap.header %> + <%= @map.to_html %> + + <%= @map.div(:width => 600, :height => 400) %> + + +First you must output the header, used by all the maps of the page: It includes the Google Maps API JS code and the JS helper functions of YM4R/GM. Then we initialize the map by calling @map.to_html. In the body, we need a DIV whose +id+ is the same as the one passed to the GMap constructor in the controller. The call to @map.div outputs such a DIV. We pass to it options to set the width and height of the map in the style attribute of the DIV. + +Note that you need to set a size for the map DIV element at some point or the map will not be displayed. You have a few ways to do this: +- You define it yourself, wherever you want. Usually as part of the layout definition in an external CSS. +- In the head of the document, through a CSS instruction output by @map.header_width_height, to which you pass 2 arguments (width and height). +- When outputting the DIV with @map.div, you can also pass an option hash, with keys :width and :height and a style attribute for the DIV element will be output. + +You can update the map trough RJS. Here is an action you can call from a link_remote_tag which would do this: + def update + @map = Variable.new("map") + @marker = GMarker.new([75.89,-42.767],:title => "Update", :info_window => "I have been placed through RJS") + end + +Here, I first bind the Ruby @map variable to the JS variable map, which already exists in the client browser. +map+ is by default the name given to a map created from YM4R/GM (this could be overriden by passing a second argument to the GMap constructor). Then I create a marker. + +And you would have inside the RJS template for the action: + page << @map.clear_overlays + page << @map.add_overlay(@marker) + +Here I first clear the map of all overlays. Then I add the marker. Note that the +overlay_init+ is not used anymore since, as its name indicates, this method is only for the initialization of the map. + +==Relation between the YM4R gem and the YM4R/GM plugin +They are completely independent from each other. + +With the plugin, you don't need the YM4R gem anymore, unless you want to use the tilers or the Ruby helpers for the Yahoo! Maps Building Block API's and the Google Maps geocoding API. Please refer to the documentation of the YM4R gem to know more about the functionalities in it. + +Conversely, the YM4R gem does not need the plugin to work. + +==Installation +Install like any other Rails plugin: + ruby script/plugin install svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm + +As part of the installation procedure, the JavaScript files found in the PLUGIN_ROOT/javascript directory will be copied to the RAILS_ROOT/public/javascripts/ directory. + +Moreover a gmaps_api_key.yml file will also be created in the RAILS_ROOT/config/ folder. If this file already exists (installed for example by a previous version of the plugin), nothing will be done to it, so you can keep your configuration data even after updating the plugin. This file is a YAML representation of a hash, similar to the database.yml file in that the primary keys in the file are the different environments (development, test, production), since you will probably need to have different Google Maps API keys depending on that criteria (for example: a key for localhost for development and test; a key for your host for production). If you don't have any special need, there will be only one key associated with each environment. If however, you need to have multiple keys (for example if your app will be accessible from multiple URLs, for which you need different keys), you can also associate a hash to the environment, whose keys will be the different hosts. In this case, you will need to pass a value to the :host key when calling the method GMap.header (usually @request.host). + +==Migration from the YM4R gem versions <= 0.4.1 +Apart from the installation of the plugin detailed above, you will also need to delete the instructions to require the file and include the Ym4r::GoogleMaps module in your controllers: + require 'ym4r' + include Ym4r::GoogleMaps +This lines are now not needed since the plugin is loaded as part of the normal Rails loading procedure and the module is included when the plugin is loaded. + +==Operations +You can use the library to display Google maps easily from Rails. The version of the API used is v2. The library is engineered so updates to the map through RJS/Ajax are possible. I have made available 2 in-depth tutorials to show some of the functionalities of the library and how it can be integrated with GeoRuby and the Spatial Adapter for Rails: +- http://thepochisuperstarmegashow.com/2006/06/02/ym4r-georuby-spatial-adapter-demo/ +- http://thepochisuperstarmegashow.com/2006/06/03/google-maps-yahoo-traffic-mash-up/ +Following is some notes about manipulating Google Maps with the library: + +===Naming conventions +The names of the Ruby class follow the ones in the JavaScript Google Maps API v2, except for GMap2, which in Ruby code is called simply GMap. To know what is possible to do with each class, you should refer to the documentation available on Google website. + +On top of that, you have some convenience methods for initializing the map (in the GMap class). Also, the constructors of some classes accept different types of arguments to be converted later in the correct JavaScript format. For example, the +GMarker+ aclass accepts an array of 2 floats as parameter, in addition of a GLatLng object, to indicate its position. It also facilitates the attribution of an HTML info window, displayed when the user clicks on it, since you can pass to the constructor an options hash with the :info_window key and the text to display as the value, instead of having to wire the response to a click event yourself. + +===Binding JavaScript and Ruby +Since the Google Maps API uses JavaScript to create and manipulate a map, most of what the library does is outputting JavaScript, although some convenience methods are provided to simplify some common operations at initialization time. When you create a YM4R mapping object (a Ruby object which includes the MappingObject module) and call methods on it, these calls are converted by the library into JavaScript code. At initialization time, you can pass arbitrary JavaScript code to the GMap#record_init and GMap#record_global_init.Then, at update time, if you use Ruby-on-Rails as your web framework, you can update your map through RJS by passing the result of the method calls to the page << method to have it then interpreted by the browser. + +For example, here is a typical initialization sequence for a map + @map = GMap.new("map_div") + @map.control_init(:large_map => true,:map_type => true) + @map.center_zoom_init([35.12313,-110.567],12) + @map.overlay_init GMarker.new([35.12878, -110.578],:title => "Hello!") + @map.record_init @map.add_overlay(GMarker.new([35.12878, -110.578],:title => "Hello!")) + +While +center_zoom_init+, +control_init+ or +overlay_init+ (and generally all the GMap methods which end in +init+) are one of the rare convenience methods that do not output JavaScript, the +add_overlay+ does. Actually, if you look at the code of the GMap class, you won't find any +add_overlay+ method, although in the documentation of the GMap2 class from the Google Maps API documentation, you will find something about the +addOverlay+ JavaScript method. In fact, when you call on a mapping object an unknow method, it is converted to a javascriptified version of it, along with its arguments, and a string of JavaScript code is output. So the @map.add_overlay... above is converted to "map.addOverlay(new GMarker(GLatLng.new(35.12878, -110.578),{title:\"Hello!\"}))", which is then passed to the +record_init+ method of a Ruby GMap object to be later output along with the rest of the initialization code. Any arbitrary JavaScript code can be passed to the +record_init+ method. Note that 2 last lines of the previous code sample are strictly equivalent and since the +overlay_init+ version is a bit simpler, it should be preferred. + +===Initialization of the map +The map is represented by a GMap object. You need to pass to the constructor the id of a DIV that will contain the map. You have to place this DIV yourself in your HTML template. You can also optionnally pass to the constructor the JavaScript name of the variable that will reference the map, which by default will be global in JavaScript. You have convenience methods to setup the controls, the center, the zoom, overlays, the interface configuration (continuous zoom, double click zoom, dragging), map types and the icons (which are also global). You can also pass arbitrary JavaScript to +record_init+ and +record_global_init+. Since, by default, the initialization of the map is performed in a callback function, if you want to have a globally accessible variable, you need to use the +global+ version. + +You can also have multiple maps. Just make sure you give them different DIV id's, as well as different global variable names, when constructing them: + @map1 = GMap("map1_div","map1") + @map2 = GMap("map2_div","map2") + +The other absolutely necessary initialization step in the controller is the setting of center and zoom: + @map1.center_zoom_init([49.12,-56.453],3) +Withouth this code the map will display an empty grey rectangle with Google's logo. + +Then in your template, you have 2 necessary calls: +- The static GMap.header: Outputs the inclusion of the JavaScript file from Google to make use of the Google Maps API and by default a style declaration for VML objects, necessary to display polylines under IE. This default can be overriddent by passing :with_vml => false as option to the +header+ method. You can also pass to this method a :host option in order to select the correct key for the location where your app is currently deployed, in case the current environment has multiple possible keys. Usually, in this case, you should pass it @request.host. Finally you can override all the key settings in the configuration by passing a value to the :key key. +- GMap#to_html: Outputs the initialization code of the map. By default, it outputs the +script+ tags and initializes the map in response to the onload event of the JavaScript window object. You can call +to_html+ on each one of your maps to have them all initialized. You can pass the option :full=>true to the method to setup a fullscreen map, which will also be resized when the window size changes. + +You can also use GMap#div to output the div element with the correct +id+. You can pass it options :height and :width to set the size of the div (although, as indicated below, you have other ways to do that). + +So you should have something like the following: + Hello + <%= GMap.header %> + <%= @map.to_html %> + + <%= @map.div(:width => 600,:height => 400) %> + + +Note that you need to set a size for the map DIV element at some point or the map will not be displayed. You have a few ways to do this: +- You define it yourself, wherever you want. Usually as part of the layout definition in an external CSS. +- In the head of the document, through a CSS instruction output by @map.header_width_height, to which you pass 2 arguments (width and height). +- When outputting the DIV with @map.div, you can also pass an option hash, with keys :width and :height and a style attribute for the DIV element will be output. + +===GMarkers +GMarkers are point of interests on a map. You can give a position to a GMarker constructor either by passing it a 2D-array of coordinates, a GLatLng object, an object of type Variable (which evaluates to a GLatLng when interpreted in the browser) or an address, which will be geocoded when the marker is initialized by the map. + +You can pass options to the GMarker to customize the info window (:info_window or :info_window_tabs options), the tooltip (:title option) or the icon used (:icon option). + +For example: + GMarker.new([12.4,65.6],:info_window => "I'm a Popup window",:title => "Tooltip") + GMarker.new(GLatLng.new([12.3,45.6])) + GMarker.new("Rue Clovis Paris France", :title => "geocoded") + + +===Update of the map +You are able to update the map through Ajax. In Ruby-on-Rails, you have something called RJS, which sends JavaScript created on the server as a response to an action, which is later interpreted by the browser. It is usually used for visual effects and replacing or adding elements. It can also accept arbitrary JavaScript and this is what YM4R uses. + +For example, if you want to add a marker to the map, you need to do a few things. First, you have to bind a Ruby mapping object to the global JavaScript map variable. By default its name is +map+, but you could have overriden that at initialization time. You need to do something like this: + @map = Variable.new("map") ++map+ in the Variable constructor is the name of the global JavaScript map variable. Then any method you call on @map will be converted in JavaScript to a method called on +map+. In your RJS code, you would do something like this to add a marker: + page << @map.add_overlay(GMarker.new([12.1,12.76],:title => "Hello again!")) +What is sent to the browser will be the fllowing JavaScript code: + map.addOverlay(new GMarker(new GLatLng(123123.1,12313.76),{title:\"Hello again!\"})) + + +===GPolyline +GPolylines are colored lines on the map. The constructor takes as argument a list of GLatLng or a list of 2-element arrays, which will be transformed into GLatLng for you. It can also take the color (in the #rrggbb form), the weight (an integer) and the opacity. These arguments are optional though. + +For example: + GPolyline.new([[12.4,65.6],[4.5,61.2]],"#ff0000",3,1.0) + +Then you add it like any other overlay: + @map.overlay_init(polyline) + +===GPolygon +GPolygons are colored areas on the map. As of 29/12, this feature is not documented in the official GMaps API, but thanks to Steven Talcott Smith (http://www.talcottsystems.com), it is possible to use it now in ym4r. + +The constructor takes as argument a list of GLatLng or a list of 2-element arrays, which will be transformed into GLatLng for you. Note that for polygons, the last point must be equal to the first, in order to have a closed loop. It can also take the color (in the #rrggbb form) of the stroke, the weight of the stroke, the opacity of the stroke, as well as the color of the fill and the opacity. These arguments are optional though. + +For example: + GPolygon.new([[12.4,6.6],[4.5,1.2],[-5.6,-12.4],[12.4,6.6]],"#ff0000",3,1.0,"#00ff00",1.0) + +Then you add it like any other overlay: + @map.overlay_init(polygon) + +===GMarkerGroup +A new type of GOverlay is available, called GMarkerGroup. + +To use it you would have to include in your HTML template the JavaScript file markerGroup.js after the call to GMap.header (because it extends the GOverlay class). You should have something like that in your template: + <%= javascript_include_tag("markerGroup") %> + +It is useful in 2 situations: +- Display and undisplay a group of markers without referencing all of them. You just declare your marker group globally and call +activate+ and +deactivate+ on this group in response, for example, to clicks on links on your page. +- Keeping an index of markers, for example, in order to show one of these markers in reponse to a click on a link (the way Google Local does with the results of the search). + +Here is how you would use it from your Ruby code: + @map = GMap.new("map_div") + marker1 = GMarker.new([123.55,123.988],:info_window => "Hello from 1!") + marker2 = GMarker.new([87.123,18.9],:info_window =>"Hello from 2!") + @map.overlay_global_init(GMarkerGroup.new(true, + 1 => marker1, + 2 => marker2),"myGroup") +Here I have created an active (ie which is going to be displayed when the map is created) marker group called +myGroup+ with 2 markers, which I want to reference later. If I did not want to reference them later (I just want to be able to display and undisplay the marker group), I could have passed an array instead of the hash. + +Then in your template, you could have that: + Click here to display marker1 + Click here to display marker2 + <%= @map.div %> +When you click on one of the links, the corresponding marker has its info window displayed. + +You can call +activate+ and +deactivate+ to display or undisplay a group of markers. You can add new markers with addMarker(marker,id). Again if you don't care about referencing the marker, you don't need to pass an id. If the marker group is active, the newly added marker will be displayed immediately. Otherwise it will be displayed the next time the group is activated. Finally, since it is an overlay, the group will be removed when calling clearOverlays on the GMap object. + +You can center and zoom on all the markers in the group by calling GMarkerGroup#centerAndZoomOnMarkers() after the group has been added to a map. So for example, if you would want to do that at initalization time, you would do the following (assuming your marker group has been declared as +group+): + @map.record_init group.center_and_zoom_on_markers + +===GMarkerManager +It is a recent (v2.67) GMaps API class that manages the efficient display of potentially thousands of markers. It is similar to the Clusterer (see below) since markers start appearing at specified zoom levels. The clustering behaviour has to be managed explicitly though by specifying the cluster for smaller zoom levels and specify the expanded cluster for larger zoom levels and so on. Note that it is not an overlay and is not added to the map through an overlay_init call. + +Here is an example of use: + @map = GMap.new("map_div") + @map.control_init(:large_map => true, :map_type => true) + @map.center_zoom_init([59.91106, 10.72223],3) + srand 1234 + markers1 = [] + 1.upto(20) do |i| + markers1 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-20-#{i}") + end + managed_markers1 = ManagedMarker.new(markers1,0,7) + + markers2 = [] + 1.upto(200) do |i| + markers2 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-200-#{i}") + end + managed_markers2 = ManagedMarker.new(markers2,8,9) + + markers3 = [] + 1.upto(1000) do |i| + markers3 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-300-#{i}") + end + managed_markers3= ManagedMarker.new(markers3,10) + + mm = GMarkerManager.new(@map,:managed_markers => [managed_markers1,managed_markers2,managed_markers3]) + @map.declare_init(mm,"mgr") + +===Clusterer +A Clusterer is a type of overlay that contains a potentially very large group of markers. It is engineered so markers close to each other and undistinguishable from each other at some level of zoom, appear as only one marker on the map at that level of zoom. Therefore it is possible to have a very large number of markers on the map at the same time and not having the map crawl to a halt in order to display them. It has been slightly modified from Jef Poskanzer's original Clusterer2 code (see http://www.acme.com/javascript/ for the original version). The major difference with that version is that, in YM4R, the clusterer is a GOverlay and can therefore be added to the map with map.addOverlay(clusterer) and is cleared along with the rest of overlays when map.clearOverlays() is called. + +In order to use a clusterer, you should include in your template page the clusterer.js file after the call to GMap.header (because it extends the GOverlay class). You should have something like that in your template: + <%= javascript_include_tag("clusterer") %> + +To create a clusterer, you should first create an array of all the GMarkers that you want to include in the clusterer (you can still add more after the clusterer has been added to the map though). When GMarkers close together are grouped in one cluster (represented by another marker on the map) and the user clicks on the cluster marker, a list of markers in the cluster is displayed in the info windo with a description: By default it is equal to the +title+ of the markers (which is also displayed as a tooltip when hovering on the marker with the mouse). If you don't want that, you can also pass to the GMarker constructor a key-value pair with key :description to have a description different from the title. For example, here is how you would create a clusterer: + markers = [GMarker.new([37.8,-90.4819],:info_window => "Hello",:title => "HOYOYO"), + GMarker.new([37.844,-90.47819],:info_window => "Namaste",:description => "Chopoto" , :title => "Ay"), + GMarker.new([37.83,-90.456619],:info_window => "Bonjour",:title => "Third"), + ] + clusterer = Clusterer.new(markers,:max_visible_markers => 2) +Of course, with only 3 markers, the whole clusterer thing is totally useless. Usually, using the clusterer starts being interesting with hundreds of markers. The options to pass the clusterer are: +- :max_visible_markers: Below this number of markers, no clustering is performed. Defaults to 150. +- :grid_size: The clusterer divides the visible area into a grid of this size. Defaults to 5. +- :min_markers_per_cluster: Below this number of markers a cluster is not formed. Defaults to 5. +- :max_lines_per_info_box: Number of lines to display in the info window displayed when a cluster is clicked on by the user. Defaults to 10. +- :icon: Icon to be used to mark a cluster. Defaults to G_DEFAULT_ICON (the classic red marker). + +Then to add the clusterer to the map at initialization time, you proceed as with a classic overlay: + @map.overlay_init clusterer + +To add new markers in RJS (if the clusterer has been declared globally with overlay_global_init), you should do this: + page << clusterer.add_marker(marker,description) +In this case, the :description passed to the GMarker contructor will not be taken into account. Only the +description+ passed in the call to +add_marker+ will. + +===GeoRss Overlay +An group of markers taken from a Rss feed containing location information in the W3C basic geo (WGS83 lat/lon) vocabulary and in the Simple GeoRss format. See http://georss.org for more details. The support for GeoRss relies on the MGeoRSS library by Mikel Maron (http://brainoff.com/gmaps/mgeorss.html), although a bit modified, mostly to have the GeoRssOverlay respect the GOverlay API. It has also been enhanced by Andrew Turner who added support for the GeoRss Simple format. + +Note that the geoRssOverlay.js file must be included in the HTML template in order to use this functionality. You should have something like that in your template: + <%= javascript_include_tag("geoRssOverlay") %> + +Here is how you would use it. First create the overlay at initialization: + def index + @map = GMap.new("map_div") + @map.control_init(:large_map => true) + @map.center_zoom_init([38.134557,-95.537109],0) + @map.overlay_init(GeoRssOverlay.new(url_for(:action => "earthquake_rss")) + end +Since it is not possible to make requests outside the domain where the current page comes from, there is a need for a proxy. With the GeoRssOverlay initialization above, the request will be made by the earthquake_rss action, where the address to find the RSS feed will be hardwired: + def earthquake_rss + result = Net::HTTP.get_response("earthquake.usgs.gov","/eqcenter/recenteqsww/catalogs/eqs7day-M5.xml") + render :xml => result.body + end +If you don't want to hardwire the RSS feed location in an action, you can. But you will have to pass the :proxy option to the GeoRssOverlay constructor. When requesting the RSS feed, the browser will in fact call the proxy with the actual URL of the RSS feed in the +q+ parameter of the request. Here is how you would initialize the GeoRssOverlay that way: + @map.overlay_init(GeoRssOverlay.new("http://earthquake.usgs.gov/eqcenter/recenteqsww/catalogs/eqs7day-M5.xml", + :proxy => url_for(:action => "proxy"))) +And here is an example of proxy action: + def proxy + result = Net::HTTP.get_response(URI.parse(@params[:q])) + render :xml => result.body + end +You should probably do some checks to ensure the proxy is not abused but it is of your responsibility. + +Another option can be passed to the GeoRssOverlay constructor to set an icon to be used by the markers of the overlay: :icon. By default it is equal to the default icon (the classic red one). + +In the view, you should have something like the following: + + Testing GeoRss + <%= GMap.header(:with_vml => false) %> + <%= javascript_include_tag("geoRssOverlay") %> + <%= @map.header_width_height(600,400) %> + <%= @map.to_html %> + + + <%= @map.div %> + + +Note the inclusion of the geoRssOverlay.js file. + +Other options to pass to the GeoRssOverlay constructor are the following: +- :list_div: In case you want to make a list of all the markers, with a link on which you can click in order to display the info on the marker, use this option to indicate the ID of the DIV (that you must place yourself on your page). +- :list_item_class: class of the DIV containing each item of the list. Ignored if option :list_div is not set. +- :limit: Maximum number of items to display on the map. +- :content_div: Instead of having an info window appear, indicates the ID of the DIV where this info should be displayed. + +===Adding new map types +It is now possible to easily add new map types, on top of the already existing ones, like G_SATELLITE_MAP or G_NORMAL_MAP. The imagery for these new map types can come from layers of the standard map types or can be taken either from a WMS server or from pretiled images on a server (that can be generated with a tool that comes with the YM4R gem: refer to the README of the gem to know more about it). + +For exemple, here is how you would setup layers from a public WMS server of the DMAP team of the American Navy: + layer = WMSLayer.new("http://columbo.nrlssc.navy.mil/ogcwms/servlet/WMSServlet/AccuWeather_Maps.wms", + "20:3,6:3,0:27,0:29,6:19", + :copyright => {'prefix' => "Map Copyright 2006", 'copyright_texts'=> ["DMAP"]}, + :use_geographic => true, :opacity => 0.8) +This sets up a connection to a WMS service, requesting layers 20:3,6:3,0:27,0:29,6:19 (you would have to look at the GetCapabilities document of the service to know what the valid layers are). The copyright notice attributes the data to DMAP. The images will be 80% opaque. For the rest of the options, the default values are used: default styles (:style option), PNG format (:format option), valid for all zoom levels (:zoom_range option). The option :merc_proj is irrelevant here since the :use_geographic option is true. + +The arguments :use_geographic and :merc_proj warrant some explanation. The Google Maps are in the Simple Mercator projection in the WGS84 datum and currently do not support the display of data in projections other than that (at least if you want to display markers and lines on top of it). Unfortunately, different WMS servers do not identify this projection the same way. So you can give to the WMSLayer constructor your server type through the :merc_proj option and it will figure out what is the correct identifier. Currently, this works only for :mapserver (EPSG:54004) and :geoserver (EPSG:41001). For others you can directly pass a number corresponding to the EPSG definition of the simple Mercator projection of your server. On top of that, some servers just don't support the Simple Mercator projection. This is why there is a :use_geographic option, that can be set to +true+. It is in order to tell the WMSLayer that it should request its tiles using LatLon coordinates in the WGS84 datum (which should be supported by any server in a consistant way). Unfortunately it is not perfect since the deformation is quite important for low zoom levels (< 5 for the US). Higher than that, the deformation is not that visible. However, if you control the WMS server, it is recommended that you don't use :use_geographic and instead use the :merc_proj option and setup the Mercator projection in your server if it is not done by default. + +Note that you need to include the wms-gs.js javascript file in your HTML page in order to make use of the WMSLayer functionality. You should have something like that in your template: + <%= javascript_include_tag("wms-gs") %> +This file uses code by John Deck with the participation of others (read the top of the javascript file to know more). + +Here is how to define a pretiled layer: + layer = PreTiledLayer.new("http://localhost:3000/tiles", + :copyright => {'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]}, + :zoom_range => 13..13, :opacity => 0.7, :format => "gif") +I tell the PreTiledLayer constructor where I can find the tiles, setup the Copyright string, the valid zooms for the map, the opacity and the format. Tiles must have standardized names: tile_#{zoom}_#{x_tile}_#{y_tile}.#{format} (here the format is "gif"). You can use tools found in the YM4R gem to generate tiles in this format either from local maps or from WMS servers (useful to create tiles from geographic data files without having to run a map server or to cache images from slow servers). Again refer to the documentation of the gem for more information on how to do this. + +Instead of having the tiles requested directly, you can also decide to have an action on the server which takes care of it. You can used the class PreTiledLayerFromAction for this. In this case, the first argument of the constructor is an url of an action. The arguments +x+, +y+ and +z+ will be passed to it. + layer = PreTiledLayerFromAction.new(url_for(:action => :get_tile), + :copyright => {'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]}, + :zoom_range => 13..14, :opacity => 0.7) +The other constructor arguments have the same meaning as PreTiledLayer. Here is an uninteresting example of action that serves tiles: + def get_tile + x = @params[:x] + y = @params[:y] + z = @params[:z] + begin + send_file "#{RAILS_ROOT}/public/tiles/tile_#{z}_#{x}_#{y}.png" , + :type => 'image/png', :disposition => 'inline' + rescue Exception + render :nothing => true + end + end + +You can add a layer to a new map type the following way: + map_type = GMapType.new(layer,"My WMS") +This is actually the simplest configuration possible. Your map type has only one data layer and is called "My WMS". You can add more that one layer: Either one that you have created yourself or existing ones. For example: + map_type = GMapType.new([GMapType::G_SATELLITE_MAP.get_tile_layers[0],layer,GMapType::G_HYBRID_MAP.get_tile_layers[1]], + "Test WMS") +Here for the "Test WMS" map type, we also take the first layer of the "Satellite" map type in the background and overlay the second layer of the "Hybrid" map type (roads, country boundaries, etc... transparently overlaid on top of the preceding layers) so when the "Test WMS" map type is selected in the interface, all three layers will be displayed. + +Finally to add a map type to a GMap: + @map.add_map_type_init(map_type) +If you want to wipe out the existing map types (for example the 3 default ones), you can add a +false+ argument to the +add_map_type_init+ method and the +map_type+ will be the only one. + +If you want to setup the map as the default one when the map is initially displayed, you should first declare the map type then add it to the map as indicated above and finally setting it as the default map type: + @map.declare_init(map_type,"my_map_type") + @map.add_map_type_init(map_type) + @map.set_map_type_init(map_type) +Future versions of the plugin may simplify that. + +===Google Geocoding +A helper to perform geocoding on the server side (in Ruby) is included. Here is an example of request: + results = Geocoding::get("Rue Clovis Paris") +You can also pass to the +get+ method an options hash to manage the various API key options (see the section on GMap.header for details). +results+ is an array of Geocoding::Placemark objects, with 2 additional attributes: +status+ and +name+. You should check if +status+ equals Geocoding::GEO_SUCCESS to know if the request has been successful. You can then access the various data elements. + +Here is an example that will display a marker on Paris: + results = Geocoding::get("Rue Clovis Paris") + if results.status == Geocoding::GEO_SUCCESS + coord = results[0].latlon + @map.overlay_init(GMarker.new(coord,:info_window => "Rue Clovis Paris")) + end + +You could also have performed the geocoding on the client side with the following code, which is functionnality equivalent to the code above: + GMarker.new("Rue Clovis Paris",:info_window => "Rue Clovis Paris") + + +==Recent changes +- GMarker can now be placed by address (in addition to coordinates). Some code to geocode the address when the marker is initialized is added +- Addition of a +center_zoom_on_points_init+ to center and zoom on a group of pixel +- In JS, addition of methods to GMap2 and GMarkerGroup to center and zoom on a group of points or markers (thanks to Glen Barnes) +- Support for easy setup of fullscreen maps + +==TODO +- Add support for easy manipulation of external Google Maps-related libraries: Advanced tooltip manipulation (PdMarker),... +- Addition of all GeoRss vocabularies (with all features: polylines...) to the geoRssOverlay extension +- Tutorials + +==Disclaimer +This software is not endorsed in any way by Google. + +==Acknowledgement +The YM4R/GM plugin bundles JavaScript libraries from John Deck (WMS layers on Google Maps), Jef Poskanzer (Clusterer on Google Maps) and Mikel Maron (GeoRss on Google Maps). + +==License +The YM4R/GM plugin is released under the MIT license. The clusterer.js file is redistributed with a different license (but still compatible with the MIT license). Check the top of the file in PLUGIN_ROOT/javascript to know more. + +==Support +Any questions, enhancement proposals, bug notifications or corrections can be sent to mailto:guilhem.vellut+ym4r@gmail.com. diff --git a/vendor/plugins/ym4r_gm/gmaps_api_key.yml b/vendor/plugins/ym4r_gm/gmaps_api_key.yml new file mode 100644 index 0000000..c9a004c --- /dev/null +++ b/vendor/plugins/ym4r_gm/gmaps_api_key.yml @@ -0,0 +1,7 @@ +#Fill here the Google Maps API keys for your application +#In this sample: +#For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode. +#In production, the app can be accessed through 2 different hosts: thepochisuperstarmegashow.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test). + +development: +ABQIAAAA479zRK1hoNqMcKLTMuBcTRScPKE_l4RVNk_lv74wWGSk9YyVlRQ16fPZdTl-PBiKfGdEjSpSL8gVtA diff --git a/vendor/plugins/ym4r_gm/init.rb b/vendor/plugins/ym4r_gm/init.rb new file mode 100644 index 0000000..7316aef --- /dev/null +++ b/vendor/plugins/ym4r_gm/init.rb @@ -0,0 +1,3 @@ +require 'ym4r_gm' + + diff --git a/vendor/plugins/ym4r_gm/install.rb b/vendor/plugins/ym4r_gm/install.rb new file mode 100644 index 0000000..68c5e39 --- /dev/null +++ b/vendor/plugins/ym4r_gm/install.rb @@ -0,0 +1,10 @@ +require 'fileutils' + +#Copy the Javascript files +FileUtils.copy(Dir[File.dirname(__FILE__) + '/javascript/*.js'], File.dirname(__FILE__) + '/../../../public/javascripts/') + +#copy the gmaps_api_key file +gmaps_config = File.dirname(__FILE__) + '/../../../config/gmaps_api_key.yml' +unless File.exist?(gmaps_config) + FileUtils.copy(File.dirname(__FILE__) + '/gmaps_api_key.yml.sample',gmaps_config) +end diff --git a/vendor/plugins/ym4r_gm/javascript/clusterer.js b/vendor/plugins/ym4r_gm/javascript/clusterer.js new file mode 100644 index 0000000..eeb6dd9 --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/clusterer.js @@ -0,0 +1,444 @@ +// Clusterer.js - marker clustering routines for Google Maps apps +// +// The original version of this code is available at: +// http://www.acme.com/javascript/ +// +// Copyright © 2005,2006 by Jef Poskanzer . +// All rights reserved. +// +// Modified for inclusion into the YM4R library in accordance with the +// following license: +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// For commentary on this license please see http://www.acme.com/license.html + + +// Constructor. +Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) { + this.markers = []; + if(markers){ + for(var i =0 ; i< markers.length ; i++){ + this.addMarker(markers[i]); + } + } + this.clusters = []; + this.timeout = null; + + this.maxVisibleMarkers = maxVisibleMarkers || 150; + this.gridSize = gridSize || 5; + this.minMarkersPerCluster = minMarkersPerCluster || 5; + this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10; + + this.icon = icon || G_DEFAULT_ICON; +} + +Clusterer.prototype = new GOverlay(); + +Clusterer.prototype.initialize = function ( map ){ + this.map = map; + this.currentZoomLevel = map.getZoom(); + + GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) ); + //Set map for each marker + for(var i = 0,len = this.markers.length ; i < len ; i++){ + this.markers[i].setMap( map ); + } + this.displayLater(); +} + +Clusterer.prototype.remove = function(){ + for ( var i = 0; i < this.markers.length; ++i ){ + this.removeMarker(this.markers[i]); + } +} + +Clusterer.prototype.copy = function(){ + return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox); +} + +Clusterer.prototype.redraw = function(force){ + this.displayLater(); +} + +// Call this to change the cluster icon. +Clusterer.prototype.setIcon = function ( icon ){ + this.icon = icon; +} + +// Call this to add a marker. +Clusterer.prototype.addMarker = function ( marker, description){ + marker.onMap = false; + this.markers.push( marker ); + marker.description = marker.description || description; + if(this.map != null){ + marker.setMap(this.map); + this.displayLater(); + } +}; + + +// Call this to remove a marker. +Clusterer.prototype.removeMarker = function ( marker ){ + for ( var i = 0; i < this.markers.length; ++i ) + if ( this.markers[i] == marker ){ + if ( marker.onMap ) + this.map.removeOverlay( marker ); + for ( var j = 0; j < this.clusters.length; ++j ){ + var cluster = this.clusters[j]; + if ( cluster != null ){ + for ( var k = 0; k < cluster.markers.length; ++k ) + if ( cluster.markers[k] == marker ){ + cluster.markers[k] = null; + --cluster.markerCount; + break; + } + if ( cluster.markerCount == 0 ){ + this.clearCluster( cluster ); + this.clusters[j] = null; + } + else if ( cluster == this.poppedUpCluster ) + Clusterer.rePop( this ); + } + } + this.markers[i] = null; + break; + } + this.displayLater(); +}; + +Clusterer.prototype.displayLater = function (){ + if ( this.timeout != null ) + clearTimeout( this.timeout ); + this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 ); +}; + +Clusterer.display = function ( clusterer ){ + var i, j, marker, cluster, len, len2; + + clearTimeout( clusterer.timeout ); + + var newZoomLevel = clusterer.map.getZoom(); + if ( newZoomLevel != clusterer.currentZoomLevel ){ + // When the zoom level changes, we have to remove all the clusters. + for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){ + if ( clusterer.clusters[i] != null ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + } + clusterer.clusters.length = 0; + clusterer.currentZoomLevel = newZoomLevel; + } + + // Get the current bounds of the visible area. + var bounds = clusterer.map.getBounds(); + + // Expand the bounds a little, so things look smoother when scrolling + // by small amounts. + var sw = bounds.getSouthWest(); + var ne = bounds.getNorthEast(); + var dx = ne.lng() - sw.lng(); + var dy = ne.lat() - sw.lat(); + dx *= 0.10; + dy *= 0.10; + bounds = new GLatLngBounds( + new GLatLng( sw.lat() - dy, sw.lng() - dx ), + new GLatLng( ne.lat() + dy, ne.lng() + dx ) + ); + + // Partition the markers into visible and non-visible lists. + var visibleMarkers = []; + var nonvisibleMarkers = []; + for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){ + marker = clusterer.markers[i]; + if ( marker != null ) + if ( bounds.contains( marker.getPoint() ) ) + visibleMarkers.push( marker ); + else + nonvisibleMarkers.push( marker ); + } + + // Take down the non-visible markers. + for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){ + marker = nonvisibleMarkers[i]; + if ( marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + + // Take down the non-visible clusters. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){ + clusterer.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } + } + + // Clustering! This is some complicated stuff. We have three goals + // here. One, limit the number of markers & clusters displayed, so the + // maps code doesn't slow to a crawl. Two, when possible keep existing + // clusters instead of replacing them with new ones, so that the app pans + // better. And three, of course, be CPU and memory efficient. + if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){ + // Add to the list of clusters by splitting up the current bounds + // into a grid. + var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat(); + var latInc = latRange / clusterer.gridSize; + var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 ); + for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc ) + for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){ + cluster = new Object(); + cluster.clusterer = clusterer; + cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) ); + cluster.markers = []; + cluster.markerCount = 0; + cluster.onMap = false; + cluster.marker = null; + clusterer.clusters.push( cluster ); + } + + // Put all the unclustered visible markers into a cluster - the first + // one it fits in, which favors pre-existing clusters. + for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.inCluster ){ + for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){ + cluster = clusterer.clusters[j]; + if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){ + cluster.markers.push( marker ); + ++cluster.markerCount; + marker.inCluster = true; + } + } + } + } + + // Get rid of any clusters containing only a few markers. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ) + if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + + // Shrink the clusters list. + for ( i = clusterer.clusters.length - 1; i >= 0; --i ) + if ( clusterer.clusters[i] != null ) + break; + else + --clusterer.clusters.length; + + // Ok, we have our clusters. Go through the markers in each + // cluster and remove them from the map if they are currently up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null ){ + for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){ + marker = cluster.markers[j]; + if ( marker != null && marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + } + } + + // Now make cluster-markers for any clusters that need one. + for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && cluster.marker == null ){ + // Figure out the average coordinates of the markers in this + // cluster. + var xTotal = 0.0, yTotal = 0.0; + for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){ + marker = cluster.markers[j]; + if ( marker != null ){ + xTotal += ( + marker.getPoint().lng() ); + yTotal += ( + marker.getPoint().lat() ); + } + } + var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount ); + marker = new GMarker( location, { icon: clusterer.icon } ); + cluster.marker = marker; + GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) ); + } + } + } + + // Display the visible markers not already up and not in clusters. + for ( i = 0, len = visibleMarkers.length; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.onMap && ! marker.inCluster ) + { + clusterer.map.addOverlay( marker ); + marker.addedToMap(); + marker.onMap = true; + } + } + + // Display the visible clusters not already up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){ + clusterer.map.addOverlay( cluster.marker ); + cluster.onMap = true; + } + } + + // In case a cluster is currently popped-up, re-pop to get any new + // markers into the infobox. + Clusterer.rePop( clusterer ); +}; + + +Clusterer.popUp = function ( cluster ){ + var clusterer = cluster.clusterer; + var html = ''; + var n = 0; + for ( var i = 0 , len = cluster.markers.length; i < len; ++i ) + { + var marker = cluster.markers[i]; + if ( marker != null ) + { + ++n; + html += ''; + if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox ) + { + html += ''; + break; + } + } + } + html += '
'; + if ( marker.getIcon().smallImage != null ) + html += ''; + else + html += ''; + html += '' + marker.description + '
...and ' + ( cluster.markerCount - n ) + ' more
'; + clusterer.map.closeInfoWindow(); + cluster.marker.openInfoWindowHtml( html ); + clusterer.poppedUpCluster = cluster; +}; + +Clusterer.rePop = function ( clusterer ){ + if ( clusterer.poppedUpCluster != null ) + Clusterer.popUp( clusterer.poppedUpCluster ); +}; + +Clusterer.popDown = function ( clusterer ){ + clusterer.poppedUpCluster = null; +}; + +Clusterer.prototype.clearCluster = function ( cluster ){ + var i, marker; + + for ( i = 0; i < cluster.markers.length; ++i ){ + if ( cluster.markers[i] != null ){ + cluster.markers[i].inCluster = false; + cluster.markers[i] = null; + } + } + + cluster.markers.length = 0; + cluster.markerCount = 0; + + if ( cluster == this.poppedUpCluster ) + this.map.closeInfoWindow(); + + if ( cluster.onMap ) + { + this.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } +}; + +// This returns a function closure that calls the given routine with the +// specified arg. +Clusterer.makeCaller = function ( func, arg ){ + return function () { func( arg ); }; +}; + + +// Augment GMarker so it handles markers that have been created but +// not yet addOverlayed. +GMarker.prototype.setMap = function ( map ){ + this.map = map; +}; + +GMarker.prototype.getMap = function (){ + return this.map; +} + +GMarker.prototype.addedToMap = function (){ + this.map = null; +}; + + +GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow; +GMarker.prototype.openInfoWindow = function ( node, opts ){ + if ( this.map != null ) + return this.map.openInfoWindow( this.getPoint(), node, opts ); + else + return this.origOpenInfoWindow( node, opts ); +}; + +GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml; +GMarker.prototype.openInfoWindowHtml = function ( html, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowHtml( this.getPoint(), html, opts ); + else + return this.origOpenInfoWindowHtml( html, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs; +GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts ); + else + return this.origOpenInfoWindowTabs( tabNodes, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml; +GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts ); + else + return this.origOpenInfoWindowTabsHtml( tabHtmls, opts ); +}; + +GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup; +GMarker.prototype.showMapBlowup = function ( opts ){ + if ( this.map != null ) + return this.map.showMapBlowup( this.getPoint(), opts ); + else + return this.origShowMapBlowup( opts ); +}; + + +function addDescriptionToMarker(marker, description){ + marker.description = description; + return marker; +} diff --git a/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js b/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js new file mode 100644 index 0000000..315c26d --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js @@ -0,0 +1,194 @@ +// GeoRssOverlay: GMaps API extension to display a group of markers from +// a RSS feed +// +// Copyright 2006 Mikel Maron (email: mikel_maron yahoo com) +// +// The original version of this code is called MGeoRSS and can be found +// at the following address: +// http://brainoff.com/gmaps/mgeorss.html +// +// Modified by Andrew Turner to add support for the GeoRss Simple vocabulary +// +// Modified and bundled with YM4R in accordance with the following +// license: +// +// This work is public domain + +function GeoRssOverlay(rssurl,icon,proxyurl,options){ + this.rssurl = rssurl; + this.icon = icon; + this.proxyurl = proxyurl; + if(options['visible'] == undefined) + this.visible = true; + else + this.visible = options['visible']; + this.listDiv = options['listDiv']; //ID of the item list DIV + this.contentDiv = options['contentDiv']; //ID of the content DIV + this.listItemClass = options['listItemClass']; //Class of the list item DIV + this.limitItems = options['limit']; //Maximum number of displayed entries + this.request = false; + this.markers = []; +} + +GeoRssOverlay.prototype = new GOverlay(); + +GeoRssOverlay.prototype.initialize=function(map) { + this.map = map; + this.load(); +} + +GeoRssOverlay.prototype.redraw = function(force){ + //nothing to do : the markers are already taken care of +} + +GeoRssOverlay.prototype.remove = function(){ + for(var i= 0, len = this.markers.length ; i< len; i++){ + this.map.removeOverlay(this.markers[i]); + } +} + +GeoRssOverlay.prototype.showHide=function() { + if (this.visible) { + for (var i=0;i" + title + "

" + description; + + if(this.contentDiv == undefined){ + GEvent.addListener(marker, "click", function() { + marker.openInfoWindowHtml(html); + }); + }else{ + var contentDiv = this.contentDiv; + GEvent.addListener(marker, "click", function() { + document.getElementById(contentDiv).innerHTML = html; + }); + } + + if(this.listDiv != undefined){ + var a = document.createElement('a'); + a.innerHTML = title; + a.setAttribute("href","#"); + var georss = this; + a.onclick = function(){ + georss.showMarker(index); + return false; + }; + var div = document.createElement('div'); + if(this.listItemClass != undefined){ + div.setAttribute("class",this.listItemClass); + } + div.appendChild(a); + document.getElementById(this.listDiv).appendChild(div); + } + + return marker; +} diff --git a/vendor/plugins/ym4r_gm/javascript/markerGroup.js b/vendor/plugins/ym4r_gm/javascript/markerGroup.js new file mode 100644 index 0000000..02fe624 --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/markerGroup.js @@ -0,0 +1,114 @@ +function GMarkerGroup(active, markers, markersById) { + this.active = active; + this.markers = markers || new Array(); + this.markersById = markersById || new Object(); +} + +GMarkerGroup.prototype = new GOverlay(); + +GMarkerGroup.prototype.initialize = function(map) { + this.map = map; + + if(this.active){ + for(var i = 0 , len = this.markers.length; i < len; i++) { + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } +} + +//If not already done (ie if not inactive) remove all the markers from the map +GMarkerGroup.prototype.remove = function() { + this.deactivate(); +} + +GMarkerGroup.prototype.redraw = function(force){ + //Nothing to do : markers are already taken care of +} + +//Copy the data to a new Marker Group +GMarkerGroup.prototype.copy = function() { + var overlay = new GMarkerGroup(this.active); + overlay.markers = this.markers; //Need to do deep copy + overlay.markersById = this.markersById; //Need to do deep copy + return overlay; +} + +//Inactivate the Marker group and clear the internal content +GMarkerGroup.prototype.clear = function(){ + //deactivate the map first (which removes the markers from the map) + this.deactivate(); + //Clear the internal content + this.markers = new Array(); + this.markersById = new Object(); +} + +//Add a marker to the GMarkerGroup ; Adds it now to the map if the GMarkerGroup is active +GMarkerGroup.prototype.addMarker = function(marker,id){ + if(id == undefined){ + this.markers.push(marker); + }else{ + this.markersById[id] = marker; + } + if(this.active && this.map != undefined ){ + this.map.addOverlay(marker); + } +} + +//Open the info window (or info window tabs) of a marker +GMarkerGroup.prototype.showMarker = function(id){ + var marker = this.markersById[id]; + if(marker != undefined){ + GEvent.trigger(marker,"click"); + } +} + +//Activate (or deactivate depending on the argument) the GMarkerGroup +GMarkerGroup.prototype.activate = function(active){ + active = (active == undefined) ? true : active; + if(!active){ + if(this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.removeOverlay(this.markers[i]) + } + for(var id in this.markersById){ + this.map.removeOverlay(this.markersById[id]); + } + } + this.active = false; + } + }else{ + if(!this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } + this.active = true; + } + } +} + +GMarkerGroup.prototype.centerAndZoomOnMarkers = function() { + if(this.map != undefined){ + //merge markers and markersById + var tmpMarkers = this.markers.slice(); + for (var id in this.markersById){ + tmpMarkers.push(this.markersById[id]); + } + if(tmpMarkers.length > 0){ + this.map.centerAndZoomOnMarkers(tmpMarkers); + } + } +} + +//Deactivate the Group Overlay (convenience method) +GMarkerGroup.prototype.deactivate = function(){ + this.activate(false); +} diff --git a/vendor/plugins/ym4r_gm/javascript/wms-gs.js b/vendor/plugins/ym4r_gm/javascript/wms-gs.js new file mode 100644 index 0000000..c67146b --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/wms-gs.js @@ -0,0 +1,69 @@ +/* + * Call generic wms service for GoogleMaps v2 + * John Deck, UC Berkeley + * Inspiration & Code from: + * Mike Williams http://www.econym.demon.co.uk/googlemaps2/ V2 Reference & custommap code + * Brian Flood http://www.spatialdatalogic.com/cs/blogs/brian_flood/archive/2005/07/11/39.aspx V1 WMS code + * Kyle Mulka http://blog.kylemulka.com/?p=287 V1 WMS code modifications + * http://search.cpan.org/src/RRWO/GPS-Lowrance-0.31/lib/Geo/Coordinates/MercatorMeters.pm + * + * Modified by Chris Holmes, TOPP to work by default with GeoServer. + * + * Bundled with YM4R with John Deck's permission. + * Slightly modified to fit YM4R. + * See johndeck.blogspot.com for the original version and for examples and instructions of how to use it. + */ + +var WGS84_SEMI_MAJOR_AXIS = 6378137.0; //equatorial radius +var WGS84_ECCENTRICITY = 0.0818191913108718138; +var DEG2RAD=0.0174532922519943; +var PI=3.14159267; + +function dd2MercMetersLng(p_lng) { + return WGS84_SEMI_MAJOR_AXIS * (p_lng*DEG2RAD); +} + +function dd2MercMetersLat(p_lat) { + var lat_rad = p_lat * DEG2RAD; + return WGS84_SEMI_MAJOR_AXIS * Math.log(Math.tan((lat_rad + PI / 2) / 2) * Math.pow( ((1 - WGS84_ECCENTRICITY * Math.sin(lat_rad)) / (1 + WGS84_ECCENTRICITY * Math.sin(lat_rad))), (WGS84_ECCENTRICITY/2))); +} + +function addWMSPropertiesToLayer(tile_layer,base_url,layers,styles,format,merc_proj,use_geo){ + tile_layer.format = format; + tile_layer.baseURL = base_url; + tile_layer.styles = styles; + tile_layer.layers = layers; + tile_layer.mercatorEpsg = merc_proj; + tile_layer.useGeographic = use_geo; + return tile_layer; +} + +getTileUrlForWMS=function(a,b,c) { + var lULP = new GPoint(a.x*256,(a.y+1)*256); + var lLRP = new GPoint((a.x+1)*256,a.y*256); + var lUL = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lULP,b,c); + var lLR = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lLRP,b,c); + + if (this.useGeographic){ + var lBbox=lUL.x+","+lUL.y+","+lLR.x+","+lLR.y; + var lSRS="EPSG:4326"; + }else{ + var lBbox=dd2MercMetersLng(lUL.x)+","+dd2MercMetersLat(lUL.y)+","+dd2MercMetersLng(lLR.x)+","+dd2MercMetersLat(lLR.y); + var lSRS="EPSG:" + this.mercatorEpsg; + } + var lURL=this.baseURL; + lURL+="?REQUEST=GetMap"; + lURL+="&SERVICE=WMS"; + lURL+="&VERSION=1.1.1"; + lURL+="&LAYERS="+this.layers; + lURL+="&STYLES="+this.styles; + lURL+="&FORMAT=image/"+this.format; + lURL+="&BGCOLOR=0xFFFFFF"; + lURL+="&TRANSPARENT=TRUE"; + lURL+="&SRS="+lSRS; + lURL+="&BBOX="+lBbox; + lURL+="&WIDTH=256"; + lURL+="&HEIGHT=256"; + lURL+="&reaspect=false"; + return lURL; +} diff --git a/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js b/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js new file mode 100644 index 0000000..1c768df --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js @@ -0,0 +1,117 @@ +// JS helper functions for YM4R + +function addInfoWindowToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowHtml(info,options);}); + return marker; +} + +function addInfoWindowTabsToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowTabsHtml(info,options);}); + return marker; +} + +function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){ + layer.getTileUrl = getTile; + layer.getCopyright = copyright; + layer.getOpacity = opacity; + layer.isPng = isPng; + return layer; +} + +function addOptionsToIcon(icon,options){ + for(var k in options){ + icon[k] = options[k]; + } + return icon; +} + +function addCodeToFunction(func,code){ + if(func == undefined) + return code; + else{ + return function(){ + func(); + code(); + } + } +} + +function addGeocodingToMarker(marker,address){ + marker.orig_initialize = marker.initialize; + orig_redraw = marker.redraw; + marker.redraw = function(force){}; //empty the redraw method so no error when called by addOverlay. + marker.initialize = function(map){ + new GClientGeocoder().getLatLng(address, + function(latlng){ + if(latlng){ + marker.redraw = orig_redraw; + marker.orig_initialize(map); //init before setting point + marker.setPoint(latlng); + }//do nothing + }); + }; + return marker; +} + + + +GMap2.prototype.centerAndZoomOnMarkers = function(markers) { + var bounds = new GLatLngBounds(markers[0].getPoint(), + markers[0].getPoint()); + for (var i=1, len = markers.length ; i:key) or a host, (:host). + def self.get(request,options = {}) + api_key = ApiKey.get(options) + output = options[:output] || "kml" + url = "http://maps.google.com/maps/geo?q=#{URI.encode(request)}&key=#{api_key}&output=#{output}" + + res = open(url).read + + case output.to_sym + when :json + res = eval(res.gsub(":","=>")) #!!!EVAL EVAL EVAL EVAL!!! hopefully we can trust google... + placemarks = Placemarks.new(res['name'],res['Status']['code']) + if res['Placemark'] + placemark = res['Placemark'] + + placemark.each do |data| + + data_country = data['Country']['CountryNameCode'] rescue "" + data_administrative = data['Country']['AdministrativeArea']['AdministrativeAreaName'] rescue "" + data_sub_administrative = data['Country']['AdministrativeArea']['SubAdministrativeArea']['SubAdministrativeAreaName'] rescue "" + data_locality = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['LocalityName'] rescue "" + data_dependent_locality = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['DependentLocalityName'] rescue "" + data_thoroughfare = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['Thoroughfare']['ThoroughfareName'] rescue "" + data_postal_code = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['Thoroughfare']['PostalCode']['PostalCodeNumber'] rescue "" + lon, lat = data['Point']['coordinates'][0,2] + data_accuracy = data['Accuracy'] + unless data_accuracy.nil? + data_accuracy = data_accuracy.to_i + end + + placemarks << Geocoding::Placemark.new(data['address'], + data_country, + data_administrative, + data_sub_administrative, + data_locality, + data_dependent_locality, + data_thoroughfare, + data_postal_code, + lon, lat, data_accuracy) + + end + end + when :kml, :xml + + doc = REXML::Document.new(res) + + response = doc.elements['//Response'] + placemarks = Placemarks.new(response.elements['name'].text,response.elements['Status/code'].text.to_i) + response.elements.each(".//Placemark") do |placemark| + data = placemark.elements + data_country = data['.//CountryNameCode'] + data_administrative = data['.//AdministrativeAreaName'] + data_sub_administrative = data['.//SubAdministrativeAreaName'] + data_locality = data['.//LocalityName'] + data_dependent_locality = data['.//DependentLocalityName'] + data_thoroughfare = data['.//ThoroughfareName'] + data_postal_code = data['.//PostalCodeNumber'] + lon, lat = data['.//coordinates'].text.split(",")[0..1].collect {|l| l.to_f } + data_accuracy = data['.//*[local-name()="AddressDetails"]'].attributes['Accuracy'] + unless data_accuracy.nil? + data_accuracy = data_accuracy.to_i + end + placemarks << Geocoding::Placemark.new(data['address'].text, + data_country.nil? ? "" : data_country.text, + data_administrative.nil? ? "" : data_administrative.text, + data_sub_administrative.nil? ? "" : data_sub_administrative.text, + data_locality.nil? ? "" : data_locality.text, + data_dependent_locality.nil? ? "" : data_dependent_locality.text, + data_thoroughfare.nil? ? "" : data_thoroughfare.text, + data_postal_code.nil? ? "" : data_postal_code.text, + lon, lat, data_accuracy ) + end + end + + placemarks + end + + #Group of placemarks returned by the Geocoding service. If the result is valid the +status+ attribute should be equal to Geocoding::GE0_SUCCESS + class Placemarks < Array + attr_accessor :name,:status + + def initialize(name,status) + super(0) + @name = name + @status = status + end + end + + #A result from the Geocoding service. + class Placemark < Struct.new(:address,:country_code,:administrative_area,:sub_administrative_area,:locality,:dependent_locality,:thoroughfare,:postal_code,:longitude,:latitude,:accuracy) + def lonlat + [longitude,latitude] + end + + def latlon + [latitude,longitude] + end + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb new file mode 100644 index 0000000..ddaf8bb --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb @@ -0,0 +1,41 @@ + +Ym4r::GmPlugin::GPolyline.class_eval do + #Creates a GPolyline object from a georuby line string. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(line_string,color = nil,weight = nil,opacity = nil) + GPolyline.new(line_string.points.collect { |point| GLatLng.new([point.y,point.x])},color,weight,opacity) + end +end + +Ym4r::GmPlugin::GMarker.class_eval do + #Creates a GMarker object from a georuby point. Accepts the same options as the GMarker constructor. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(point,options = {}) + GMarker.new([point.y,point.x],options) + end +end + +Ym4r::GmPlugin::GLatLng.class_eval do + #Creates a GLatLng object from a georuby point. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(point,unbounded = nil) + GLatLng.new([point.y,point.x],unbounded) + end +end + +Ym4r::GmPlugin::GLatLngBounds.class_eval do + #Creates a GLatLng object from a georuby point. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(envelope) + GLatLngBounds.new(GLatLng.from_georuby(envelope.lower_corner), + GLatLng.from_georuby(envelope.upper_corner)) + end +end + +Ym4r::GmPlugin::GPolygon.class_eval do + #Creates a GPolygon object from a georuby polygon or line string. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(ls_or_p, stroke_color="#000000",stroke_weight=1,stroke_opacity=1.0,color="#ff0000",opacity=1.0) + if ls_or_p.is_a?(GeoRuby::SimpleFeatures::LineString) + GPolygon.new(ls_or_p.collect { |point| GLatLng.new([point.y,point.x])},stroke_color,stroke_weight,stroke_opacity,color,opacity) + else + GPolygon.new(ls_or_p[0].collect { |point| GLatLng.new([point.y,point.x])},stroke_color,stroke_weight,stroke_opacity,color,opacity) + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb new file mode 100644 index 0000000..0de9c18 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb @@ -0,0 +1,37 @@ +module Ym4r + module GmPlugin + class GMapsAPIKeyConfigFileNotFoundException < StandardError + end + + class AmbiguousGMapsAPIKeyException < StandardError + end + + #Class fo the manipulation of the API key + class ApiKey + #Read the API key config for the current ENV + unless File.exist?(RAILS_ROOT + '/config/gmaps_api_key.yml') + raise GMapsAPIKeyConfigFileNotFoundException.new("File RAILS_ROOT/config/gmaps_api_key.yml not found") + else + env = ENV['RAILS_ENV'] || RAILS_ENV + GMAPS_API_KEY = YAML.load_file(RAILS_ROOT + '/config/gmaps_api_key.yml')[env] + end + + def self.get(options = {}) + if options.has_key?(:key) + options[:key] + elsif GMAPS_API_KEY.is_a?(Hash) + #For this environment, multiple hosts are possible. + #:host must have been passed as option + if options.has_key?(:host) + GMAPS_API_KEY[options[:host]] + else + raise AmbiguousGMapsAPIKeyException.new(GMAPS_API_KEY.keys.join(",")) + end + else + #Only one possible key: take it and ignore the :host option if it is there + GMAPS_API_KEY + end + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb new file mode 100644 index 0000000..bab6625 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb @@ -0,0 +1,125 @@ +module Ym4r + module GmPlugin + #Map types of the map + class GMapType + include MappingObject + + G_NORMAL_MAP = Variable.new("G_NORMAL_MAP") + G_SATELLITE_MAP = Variable.new("G_SATELLITE_MAP") + G_HYBRID_MAP = Variable.new("G_HYBRID_MAP") + + attr_accessor :layers, :name, :projection, :options + + #The options can be any of the GMapType options detailed in the documentation + a :projection. + def initialize(layers, name, options = {}) + @layers = layers + @name = name + @projection = options.delete(:projection) || GMercatorProjection.new + @options = options + end + + def create + "new GMapType(#{MappingObject.javascriptify_variable(Array(layers))}, #{MappingObject.javascriptify_variable(projection)}, #{MappingObject.javascriptify_variable(name)}, #{MappingObject.javascriptify_variable(options)})" + end + end + + #Represents a mercator projection for zoom levels 0 to 17 (more than that by passing an argument to the constructor) + class GMercatorProjection + include MappingObject + + attr_accessor :n + + def initialize(n = nil) + @n = n + end + + def create + if n.nil? + return "G_NORMAL_MAP.getProjection()" + else + "new GMercatorProjection(#{@n})" + end + end + end + + #Abstract Tile layer. Subclasses must implement a get_tile_url method. + class GTileLayer + include MappingObject + + attr_accessor :opacity, :zoom_range, :copyright, :format + + #Options are the following, with default values: + #:zoom_range (0..17), :copyright ({'prefix' => '', 'copyright_texts' => [""]}), :opacity (1.0), :format ("png") + def initialize(options = {}) + @opacity = options[:opacity] || 1.0 + @zoom_range = options[:zoom_range] || (0..17) + @copyright = options[:copyright] || {'prefix' => '', 'copyright_texts' => [""]} + @format = (options[:format] || "png").to_s + end + + def create + "addPropertiesToLayer(new GTileLayer(new GCopyrightCollection(\"\"),#{zoom_range.begin},#{zoom_range.end}),#{get_tile_url},function(a,b) {return #{MappingObject.javascriptify_variable(@copyright)};}\n,function() {return #{@opacity};},function(){return #{@format == "png"};})" + end + + #for subclasses to implement + def get_tile_url + end + end + + #Represents a pre tiled layer, taking images directly from a server, without using a server script. + class PreTiledLayer < GTileLayer + attr_accessor :base_url + + #Possible options are the same as for the GTileLayer constructor + def initialize(base_url,options = {}) + super(options) + @base_url = base_url + end + + #Returns the code to determine the url to fetch the tile. Follows the convention adopted by the tiler: {base_url}/tile_{b}_{a.x}_{a.y}.{format} + def get_tile_url + "function(a,b) { return '#{@base_url}/tile_' + b + '_' + a.x + '_' + a.y + '.#{format}';}" + end + end + + #Represents a pretiled layer (it actually does not really matter where the tiles come from). Calls an action on the server to get back the tiles. It passes the action arguments x, y (coordinates of the tile) and z (zoom level). It can be used, for example, to return default tiles when the requested tile is not present. + class PreTiledLayerFromAction < PreTiledLayer + def get_tile_url + "function(a,b) { return '#{base_url}?x=' + a.x + '&y=' + a.y + '&z=' + b ;}" + end + end + + #Represents a TileLayer where the tiles are generated dynamically from a WMS server (MapServer, GeoServer,...) + #You need to include the JavaScript file wms-gs.js for this to work + #see http://docs.codehaus.org/display/GEOSDOC/Google+Maps + class WMSLayer < GTileLayer + attr_accessor :base_url, :layers, :styles, :merc_proj, :use_geographic + + #Options are the same as with GTileLayer + :styles (""), :merc_proj (:mapserver), :use_geographic (false) + def initialize(base_url, layers, options = {}) + super(options) + @base_url = base_url.gsub(/\?$/,"") #standardize the url + @layers = layers + @styles = options[:styles] || "" + merc_proj = options[:merc_proj] || :mapserver + @merc_proj = if merc_proj == :mapserver + "54004" + elsif merc_proj == :geoserver + "41001" + else + merc_proj.to_s + end + @use_geographic = options.has_key?(:use_geographic)? options[:use_geographic] : false + puts format + end + + def get_tile_url + "getTileUrlForWMS" + end + + def create + "addWMSPropertiesToLayer(#{super},#{MappingObject.javascriptify_variable(@base_url)},#{MappingObject.javascriptify_variable(@layers)},#{MappingObject.javascriptify_variable(@styles)},#{MappingObject.javascriptify_variable(format)},#{MappingObject.javascriptify_variable(@merc_proj)},#{MappingObject.javascriptify_variable(@use_geographic)})" + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb new file mode 100644 index 0000000..82d1374 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb @@ -0,0 +1,268 @@ +module Ym4r + module GmPlugin + #Representing the Google Maps API class GMap2. + class GMap + include MappingObject + + #A constant containing the declaration of the VML namespace, necessary to display polylines under IE. + VML_NAMESPACE = "xmlns:v=\"urn:schemas-microsoft-com:vml\"" + + #The id of the DIV that will contain the map in the HTML page. + attr_reader :container + + #By default the map in the HTML page will be globally accessible with the name +map+. + def initialize(container, variable = "map") + @container = container + @variable = variable + @init = [] + @init_end = [] #for stuff that must be initialized at the end (controls) + @init_begin = [] #for stuff that must be initialized at the beginning (center + zoom) + @global_init = [] + end + + #Deprecated. Use the static version instead. + def header(with_vml = true) + GMap.header(:with_vml => with_vml) + end + + #Outputs the header necessary to use the Google Maps API, by including the JS files of the API, as well as a file containing YM4R/GM helper functions. By default, it also outputs a style declaration for VML elements. This default can be overriddent by passing :with_vml => false as option to the method. You can also pass a :host option in order to select the correct API key for the location where your app is currently running, in case the current environment has multiple possible keys. Usually, in this case, you should pass it @request.host. If you have defined only one API key for the current environment, the :host option is ignored. Finally you can override all the key settings in the configuration by passing a value to the :key key. Finally, you can pass a language for the map type buttons with the :hl option (possible values are: Japanese (ja), French (fr), German (de), Italian (it), Spanish (es), Catalan (ca), Basque (eu) and Galician (gl): no values means english) + def self.header(options = {}) + options[:with_vml] = true unless options.has_key?(:with_vml) + options[:hl] ||= '' + api_key = ApiKey.get(options) + a = "\n" + a << "\n" unless options[:without_js] + a << "" if options[:with_vml] + a + end + + #Outputs the

which has been configured to contain the map. You can pass :width and :height as options to output this in the style attribute of the DIV element (you could also achieve the same effect by putting the dimension info into a CSS or using the instance method GMap#header_width_height). You can aslo pass :class to set the classname of the div. + def div(options = {}) + attributes = "id=\"#{@container}\" " + if options.has_key?(:height) && options.has_key?(:width) + attributes += "style=\"width:#{options.delete(:width)}px;height:#{options.delete(:height)}px\" " + end + if options.has_key?(:class) + attributes += options.keys.map {|opt| "#{opt}=\"#{options[opt]}\"" }.join(" ") + end + "
" + end + + #Outputs a style declaration setting the dimensions of the DIV container of the map. This info can also be set manually in a CSS. + def header_width_height(width,height) + "" + end + + #Records arbitrary JavaScript code and outputs it during initialization inside the +load+ function. + def record_init(code) + @init << code + end + + #Initializes the controls: you can pass a hash with keys :small_map, :large_map, :small_zoom, :scale, :map_type and :overview_map and a boolean value as the value (usually true, since the control is not displayed by default) + def control_init(controls = {}) + @init_end << add_control(GSmallMapControl.new) if controls[:small_map] + @init_end << add_control(GLargeMapControl.new) if controls[:large_map] + @init_end << add_control(GSmallZoomControl.new) if controls[:small_zoom] + @init_end << add_control(GScaleControl.new) if controls[:scale] + @init_end << add_control(GMapTypeControl.new) if controls[:map_type] + @init_end << add_control(GOverviewMapControl.new) if controls[:overview_map] + end + + #Initializes the interface configuration: double-click zoom, dragging, continuous zoom,... You can pass a hash with keys :dragging, :info_window, :double_click_zoom, :continuous_zoom and :scroll_wheel_zoom. The values should be true or false. Check the google maps API doc to know what the default values are. + def interface_init(interface = {}) + if !interface[:dragging].nil? + if interface[:dragging] + @init << enableDragging() + else + @init << disableDragging() + end + end + if !interface[:info_window].nil? + if interface[:info_window] + @init << enableInfoWindow() + else + @init << disableInfoWindow() + end + end + if !interface[:double_click_zoom].nil? + if interface[:double_click_zoom] + @init << enableDoubleClickZoom() + else + @init << disableDoubleClickZoom() + end + end + if !interface[:continuous_zoom].nil? + if interface[:continuous_zoom] + @init << enableContinuousZoom() + else + @init << disableContinuousZoom() + end + end + if !interface[:scroll_wheel_zoom].nil? + if interface[:scroll_wheel_zoom] + @init << enableScrollWheelZoom() + else + @init << disableScrollWheelZoom() + end + end + end + + #Initializes the initial center and zoom of the map. +center+ can be both a GLatLng object or a 2-float array. + def center_zoom_init(center, zoom) + if center.is_a?(GLatLng) + @init_begin << set_center(center,zoom) + else + @init_begin << set_center(GLatLng.new(center),zoom) + end + end + + #Center and zoom based on the coordinates passed as argument (either 2D arrays or GLatLng objects) + def center_zoom_on_points_init(*points) + if(points.length > 0) + if(points[0].is_a?(Array)) + points = points.collect { |point| GLatLng.new(point) } + end + @init_begin << center_and_zoom_on_points(points) + end + end + + #Center and zoom based on the bbox corners. Pass a GLatLngBounds object, an array of 2D coordinates (sw and ne) or an array of GLatLng objects (sw and ne). + def center_zoom_on_bounds_init(latlngbounds) + if(latlngbounds.is_a?(Array)) + if latlngbounds[0].is_a?(Array) + latlngbounds = GLatLngBounds.new(GLatLng.new(latlngbounds[0]),GLatLng.new(latlngbounds[1])) + elsif latlngbounds[0].is_a?(GLatLng) + latlngbounds = GLatLngBounds.new(*latlngbounds) + end + end + #else it is already a latlngbounds object + + @init_begin << center_and_zoom_on_bounds(latlngbounds) + end + + #Initializes the map by adding an overlay (marker or polyline). + def overlay_init(overlay) + @init << add_overlay(overlay) + end + + #Sets up a new map type. If +add+ is false, all the other map types of the map are wiped out. If you want to access the map type in other methods, you should declare the map type first (with +declare_init+). + def add_map_type_init(map_type, add = true) + unless add + @init << get_map_types.set_property(:length,0) + end + @init << add_map_type(map_type) + end + #for legacy purpose + alias :map_type_init :add_map_type_init + + #Sets the map type displayed by default after the map is loaded. It should be known from the map (ie either the default map types or a user-defined map type added with add_map_type_init). Use set_map_type_init(GMapType::G_SATELLITE_MAP) or set_map_type_init(GMapType::G_HYBRID_MAP) to initialize the map with repsecitvely the Satellite view and the hybrid view. + def set_map_type_init(map_type) + @init << set_map_type(map_type) + end + + #Locally declare a MappingObject with variable name "name" + def declare_init(variable, name) + @init << variable.declare(name) + end + + #Records arbitrary JavaScript code and outputs it during initialization outside the +load+ function (ie globally). + def record_global_init(code) + @global_init << code + end + + #Deprecated. Use icon_global_init instead. + def icon_init(icon , name) + icon_global_init(icon , name) + end + + #Initializes an icon and makes it globally accessible through the JavaScript variable of name +variable+. + def icon_global_init(icon , name, options = {}) + declare_global_init(icon,name,options) + end + + #Registers an event + def event_init(object,event,callback) + @init << "GEvent.addListener(#{object.to_javascript},\"#{MappingObject.javascriptify_method(event.to_s)}\",#{callback});" + end + + #Registers an event globally + def event_global_init(object,event,callback) + @global_init << "GEvent.addListener(#{object.to_javascript},\"#{MappingObject.javascriptify_method(event.to_s)}\",#{callback});" + end + + #Declares the overlay globally with name +name+ + def overlay_global_init(overlay,name, options = {}) + declare_global_init(overlay,name, options) + @init << add_overlay(overlay) + end + + #Globally declare a MappingObject with variable name "name". Option :local_construction should be passed if the construction has to be done inside the onload callback method (for exsample if it depends on the GMap to be initialized) + def declare_global_init(variable,name, options = {}) + unless options[:local_construction] + @global_init << "var #{variable.assign_to(name)}" + else + @global_init << "var #{name};" + @init << variable.assign_to(name) + end + end + + #Outputs the initialization code for the map. By default, it outputs the script tags, performs the initialization in response to the onload event of the window and makes the map globally available. If you pass +true+ to the option key :full, the map will be setup in full screen, in which case it is not necessary (but not harmful) to set a size for the map div. + def to_html(options = {}) + no_load = options[:no_load] + no_script_tag = options[:no_script_tag] + no_declare = options[:no_declare] + no_global = options[:no_global] + fullscreen = options[:full] + load_pr = options[:proto_load] #to prevent some problems when the onload event callback from Prototype is used + + html = "" + html << "" if !no_script_tag + + if fullscreen + #setting up the style in case of full screen + html << "" + end + + html + end + + #Outputs in JavaScript the creation of a GMap2 object + def create + "new GMap2(document.getElementById(\"#{@container}\"))" + end + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb new file mode 100644 index 0000000..66d825e --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb @@ -0,0 +1,128 @@ +module Ym4r + module GmPlugin + #The module where all the Ruby-to-JavaScript conversion takes place. It is included by all the classes in the YM4R library. + module MappingObject + #The name of the variable in JavaScript space. + attr_reader :variable + + #Creates javascript code for missing methods + takes care of listeners + def method_missing(name,*args) + str_name = name.to_s + if str_name =~ /^on_(.*)/ + if args.length != 1 + raise ArgumentError("Only 1 argument is allowed on on_ methods"); + else + Variable.new("GEvent.addListener(#{to_javascript},\"#{MappingObject.javascriptify_method($1)}\",#{args[0]})") + end + else + args.collect! do |arg| + MappingObject.javascriptify_variable(arg) + end + Variable.new("#{to_javascript}.#{MappingObject.javascriptify_method(str_name)}(#{args.join(",")})") + end + end + + #Creates javascript code for array or hash indexing + def [](index) #index could be an integer or string + return Variable.new("#{to_javascript}[#{MappingObject.javascriptify_variable(index)}]") + end + + #Transforms a Ruby object into a JavaScript string : MAppingObject, String, Array, Hash and general case (using to_s) + def self.javascriptify_variable(arg) + if arg.is_a?(MappingObject) + arg.to_javascript + elsif arg.is_a?(String) + "\"#{MappingObject.escape_javascript(arg)}\"" + elsif arg.is_a?(Array) + "[" + arg.collect{ |a| MappingObject.javascriptify_variable(a)}.join(",") + "]" + elsif arg.is_a?(Hash) + "{" + arg.to_a.collect do |v| + "#{MappingObject.javascriptify_method(v[0].to_s)} : #{MappingObject.javascriptify_variable(v[1])}" + end.join(",") + "}" + elsif arg.nil? + "undefined" + else + arg.to_s + end + end + + #Escape string to be used in JavaScript. Lifted from rails. + def self.escape_javascript(javascript) + javascript.gsub(/\r\n|\n|\r/, "\\n").gsub("\"") { |m| "\\#{m}" } + end + + #Transform a ruby-type method name (like add_overlay) to a JavaScript-style one (like addOverlay). + def self.javascriptify_method(method_name) + method_name.gsub(/_(\w)/){|s| $1.upcase} + end + + #Declares a Mapping Object bound to a JavaScript variable of name +variable+. + def declare(variable) + @variable = variable + "var #{@variable} = #{create};" + end + + #declare with a random variable name + def declare_random(init,size = 8) + s = init.clone + 6.times { s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr } + declare(s) + end + + #Checks if the MappinObject has been declared + def declared? + !@variable.nil? + end + + #Binds a Mapping object to a previously declared JavaScript variable of name +variable+. + def assign_to(variable) + @variable = variable + "#{@variable} = #{create};" + end + + #Assign the +value+ to the +property+ of the MappingObject + def set_property(property, value) + "#{to_javascript}.#{MappingObject.javascriptify_method(property.to_s)} = #{MappingObject.javascriptify_variable(value)}" + end + + #Returns the code to get a +property+ from the MappingObject + def get_property(property) + Variable.new("#{to_javascript}.#{MappingObject.javascriptify_method(property.to_s)}") + end + + #Returns a Javascript code representing the object + def to_javascript + unless @variable.nil? + @variable + else + create + end + end + + #Creates a Mapping Object in JavaScript. + #To be implemented by subclasses if needed + def create + end + end + + #Used to bind a ruby variable to an already existing JavaScript one. It doesn't have to be a variable in the sense "var variable" but it can be any valid JavaScript expression that has a value. + class Variable + include MappingObject + + def initialize(variable) + @variable = variable + end + #Returns the javascript expression contained in the object. + def create + @variable + end + #Returns the expression inside the Variable followed by a ";" + def to_s + @variable + ";" + end + + UNDEFINED = Variable.new("undefined") + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb new file mode 100644 index 0000000..5e0953a --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb @@ -0,0 +1,386 @@ +module Ym4r + module GmPlugin + #A graphical marker positionned through geographic coordinates (in the WGS84 datum). An HTML info window can be set to be displayed when the marker is clicked on. + class GMarker + include MappingObject + attr_accessor :point, :options, :info_window, :info_window_tabs, :address + #The +points+ argument can be either a GLatLng object or an array of 2 floats. The +options+ keys can be: :icon, :clickable, :title, :info_window and :info_window_tabs, as well as :max_width. The value of the +info_window+ key is a string of HTML code that will be displayed when the markers is clicked on. The value of the +info_window_tabs+ key is an array of GInfoWindowTab objects or a hash directly, in which case it will be transformed to an array of GInfoWindowTabs, with the keys as the tab headers and the values as the content. + def initialize(position, options = {}) + if position.is_a?(Array) + @point = GLatLng.new(position) + elsif position.is_a?(String) + @point = Variable.new("INVISIBLE") #default coordinates: won't appear anyway + @address = position + else + @point = position + end + @info_window = options.delete(:info_window) + @info_window_tabs = options.delete(:info_window_tabs) + if options.has_key?(:max_url) + @info_window_options = {:max_url => options.delete(:max_url) } + else + @info_window_options = {} + end + @options = options + end + #Creates a marker: If an info_window or info_window_tabs is present, the response to the click action from the user is setup here. + def create + if @options.empty? + creation = "new GMarker(#{MappingObject.javascriptify_variable(@point)})" + else + creation = "new GMarker(#{MappingObject.javascriptify_variable(@point)},#{MappingObject.javascriptify_variable(@options)})" + end + if @info_window && @info_window.is_a?(String) + creation = "addInfoWindowToMarker(#{creation},#{MappingObject.javascriptify_variable(@info_window)},#{MappingObject.javascriptify_variable(@info_window_options)})" + elsif @info_window_tabs && @info_window_tabs.is_a?(Hash) + creation = "addInfoWindowTabsToMarker(#{creation},#{MappingObject.javascriptify_variable(@info_window_tabs.to_a.collect{|kv| GInfoWindowTab.new(kv[0],kv[1] ) })},#{MappingObject.javascriptify_variable(@info_window_options)})" + elsif @info_window_tabs + creation = "addInfoWindowTabsToMarker(#{creation},#{MappingObject.javascriptify_variable(Array(@info_window_tabs))},#{MappingObject.javascriptify_variable(@info_window_options)})" + end + if @address.nil? + creation + else + "addGeocodingToMarker(#{creation},#{MappingObject.javascriptify_variable(@address)})" + end + end + end + + #Represents a tab to be displayed in a bubble when a marker is clicked on. + class GInfoWindowTab < Struct.new(:tab,:content) + include MappingObject + def create + "new GInfoWindowTab(#{MappingObject.javascriptify_variable(tab)},#{MappingObject.javascriptify_variable(content)})" + end + end + + #Represents a definition of an icon. You can pass rubyfied versions of the attributes detailed in the Google Maps API documentation. You can initialize global icons to be used in the application by passing a icon object, along with a variable name, to GMap#icon_init. If you want to declare an icon outside this, you will need to declare it first, since the JavaScript constructor does not accept any argument. + class GIcon + include MappingObject + DEFAULT = Variable.new("G_DEFAULT_ICON") + attr_accessor :options, :copy_base + + #Options can contain all the attributes (in rubyfied format) of a GIcon object (see Google's doc), as well as :copy_base, which indicates if the icon is copied from another one. + def initialize(options = {}) + @copy_base = options.delete(:copy_base) + @options = options + end + #Creates a GIcon. + def create + if @copy_base + c = "new GIcon(#{MappingObject.javascriptify_variable(@copy_base)})" + else + c = "new GIcon()" + end + if !options.empty? + "addOptionsToIcon(#{c},#{MappingObject.javascriptify_variable(@options)})" + else + c + end + end + end + + #A polyline. + class GPolyline + include MappingObject + attr_accessor :points,:color,:weight,:opacity + #Can take an array of +GLatLng+ or an array of 2D arrays. A method to directly build a polyline from a GeoRuby linestring is provided in the helper.rb file. + def initialize(points,color = nil,weight = nil,opacity = nil) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GLatLng.new(pt) } + else + @points = points + end + @color = color + @weight = weight + @opacity = opacity + end + #Creates a new polyline. + def create + a = "new GPolyline(#{MappingObject.javascriptify_variable(points)}" + a << ",#{MappingObject.javascriptify_variable(@color)}" if @color + a << ",#{MappingObject.javascriptify_variable(@weight)}" if @weight + a << ",#{MappingObject.javascriptify_variable(@opacity)}" if @opacity + a << ")" + end + end + + #Encoded GPolyline class + class GPolylineEncoded + include MappingObject + attr_accessor :points,:color,:weight,:opacity,:levels,:zoom_factor,:num_levels + + def initialize(options={}) + #points = options[:points] + #if !points.empty? and points[0].is_a?(Array) + # @points = points.collect { |pt| GLatLng.new(pt) } + #else + #@points = points + #end + @points = options[:points] + @color = options[:color] + @weight = options[:weight] + @opacity = options[:opacity] + @levels = options[:levels] || "BBBBBBBBBBBB" + @zoom_factor = options[:zoom_factor] || 32 + @num_levels = options[:num_levels] || 4 + end + def create + a = "new GPolyline.fromEncoded({points: #{MappingObject.javascriptify_variable(points)},\n" + a << "levels: #{MappingObject.javascriptify_variable(@levels)}," + a << "zoomFactor: #{MappingObject.javascriptify_variable(@zoom_factor)}," + a << "numLevels: #{MappingObject.javascriptify_variable(@num_levels)}" + a << ",color: #{MappingObject.javascriptify_variable(@color)}" if @color + a << ",weight: #{MappingObject.javascriptify_variable(@weight)}" if @weight + a << ",opacity: #{MappingObject.javascriptify_variable(@opacity)}" if @opacity + a << "})" + end + end + + #A basic Latitude/longitude point. + class GLatLng + include MappingObject + attr_accessor :lat,:lng,:unbounded + + def initialize(latlng,unbounded = nil) + @lat = latlng[0] + @lng = latlng[1] + @unbounded = unbounded + end + def create + unless @unbounded + "new GLatLng(#{MappingObject.javascriptify_variable(@lat)},#{MappingObject.javascriptify_variable(@lng)})" + else + "new GLatLng(#{MappingObject.javascriptify_variable(@lat)},#{MappingObject.javascriptify_variable(@lng)},#{MappingObject.javascriptify_variable(@unbounded)})" + end + end + end + + #A rectangular bounding box, defined by its south-western and north-eastern corners. + class GLatLngBounds < Struct.new(:sw,:ne) + include MappingObject + def create + "new GLatLngBounds(#{MappingObject.javascriptify_variable(sw)},#{MappingObject.javascriptify_variable(ne)})" + end + end + + #Polygon. Not documented yet in the Google Maps API + class GPolygon + include MappingObject + + attr_accessor :points,:stroke_color,:stroke_weight,:stroke_opacity,:color,:opacity + + #Can take an array of +GLatLng+ or an array of 2D arrays. A method to directly build a polygon from a GeoRuby polygon is provided in the helper.rb file. + def initialize(points,stroke_color="#000000",stroke_weight=1,stroke_opacity=1.0,color="#ff0000",opacity=1.0,encoded=false) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GLatLng.new(pt) } + else + @points = points + end + @stroke_color = stroke_color + @stroke_weight = stroke_weight + @stroke_opacity = stroke_opacity + @color = color + @opacity = opacity + end + + #Creates a new polygon + def create + a = "new GPolygon(#{MappingObject.javascriptify_variable(points)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_color)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_weight)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_opacity)}" + a << ",#{MappingObject.javascriptify_variable(@color)}" + a << ",#{MappingObject.javascriptify_variable(@opacity)}" + a << ")" + end + end + + class GPolygonEncoded + include MappingObject + + attr_accessor :polyline, :color, :opacity, :outline, :fill + + def initialize(polylines,fill=true,color="#000000",opacity=0.5,outline=false) + #force polylines to be an array + if polylines.is_a? Array + @polylines = polylines + else + @polylines = [polylines] + end + @color = color + @fill = fill + @opacity = opacity + @outline = outline + end + + #Creates a new polygon. + def create + polylines_for_polygon= [] + @polylines.each do |p| + x = "{points: #{MappingObject.javascriptify_variable(p.points)}," + x << "levels: #{MappingObject.javascriptify_variable(p.levels)}," + x << "zoomFactor: #{MappingObject.javascriptify_variable(p.zoom_factor)}," + x << "numLevels: #{MappingObject.javascriptify_variable(p.num_levels)} " + x << "}" + polylines_for_polygon << x + end + + polylines_for_polygon = "[" + polylines_for_polygon.join(",") + "]" + + a = "new GPolygon.fromEncoded({polylines: #{polylines_for_polygon}," + a << "fill: #{MappingObject.javascriptify_variable(@fill)}," + a << "color: #{MappingObject.javascriptify_variable(@color)}," + a << "opacity: #{MappingObject.javascriptify_variable(@opacity)}," + a << "outline: #{MappingObject.javascriptify_variable(@outline)}" + a << "})" + end + end + + class ELabel + attr_accessor :point, :text, :style + include MappingObject + + def initialize(point, text=nil, style=nil) + @point = point + @text = text + @style = style + end + + def create + a = "new ELabel(#{MappingObject.javascriptify_variable(@point)}" + a << ",#{MappingObject.javascriptify_variable(@text)}" if @text + a << ",#{MappingObject.javascriptify_variable(@style)}" if @style + a << ")" + end + end + + + #A GGeoXml object gets data from a GeoRSS or KML feed and displays it. Use overlay_init to add it to a map at initialization time. + class GGeoXml + include MappingObject + + attr_accessor :url + + def initialize(url) + @url = url + end + + def create + "new GGeoXml(#{MappingObject.javascriptify_variable(@url)})" + end + + end + + #A GOverlay representing a group of GMarkers. The GMarkers can be identified with an id, which can be used to show the info window of a specific marker, in reponse, for example, to a click on a link. The whole group can be shown on and off at once. It should be declared global at initialization time to be useful. + class GMarkerGroup + include MappingObject + attr_accessor :active, :markers, :markers_by_id + + def initialize(active = true , markers = nil) + @active = active + @markers = [] + @markers_by_id = {} + if markers.is_a?(Array) + @markers = markers + elsif markers.is_a?(Hash) + @markers_by_id = markers + end + end + + def create + "new GMarkerGroup(#{MappingObject.javascriptify_variable(@active)},#{MappingObject.javascriptify_variable(@markers)},#{MappingObject.javascriptify_variable(@markers_by_id)})" + end + end + + #Can be used to implement a clusterer, similar to the clusterer below, except that there is more stuff to manage explicitly byt the programmer (but this is also more flexible). See the README for usage esamples. + class GMarkerManager + include MappingObject + + attr_accessor :map,:options,:managed_markers + + #options can be :border_padding, :max_zoom, :track_markers and :managed_markers: managed_markers must be an array of ManagedMarker objects + def initialize(map, options = {}) + @map = map + @managed_markers = Array(options.delete(:managed_markers)) #[] if nil + @options = options + end + + def create + puts @options.inspect + "addMarkersToManager(new GMarkerManager(#{MappingObject.javascriptify_variable(@map)},#{MappingObject.javascriptify_variable(@options)}),#{MappingObject.javascriptify_variable(@managed_markers)})" + end + + end + + #A set of similarly managed markers: They share the same minZoom and maxZoom. + class ManagedMarker + include MappingObject + + attr_accessor :markers,:min_zoom, :max_zoom + + def initialize(markers,min_zoom,max_zoom = nil) + @markers = markers + @min_zoom = min_zoom + @max_zoom = max_zoom + end + + def create + "new ManagedMarker(#{MappingObject.javascriptify_variable(@markers)},#{MappingObject.javascriptify_variable(@min_zoom)},#{MappingObject.javascriptify_variable(@max_zoom)})" + end + + end + + #Makes the link with the Clusterer2 library by Jef Poskanzer (slightly modified though). Is a GOverlay making clusters out of its GMarkers, so that GMarkers very close to each other appear as one when the zoom is low. When the zoom gets higher, the individual markers are drawn. + class Clusterer + include MappingObject + attr_accessor :markers,:icon, :max_visible_markers, :grid_size, :min_markers_per_cluster , :max_lines_per_info_box + + def initialize(markers = [], options = {}) + @markers = markers + @icon = options[:icon] || GIcon::DEFAULT + @max_visible_markers = options[:max_visible_markers] || 150 + @grid_size = options[:grid_size] || 5 + @min_markers_per_cluster = options[:min_markers_per_cluster] || 5 + @max_lines_per_info_box = options[:max_lines_per_info_box] || 10 + end + + def create + js_marker = '[' + @markers.collect do |marker| + add_description(marker) + end.join(",") + ']' + + "new Clusterer(#{js_marker},#{MappingObject.javascriptify_variable(@icon)},#{MappingObject.javascriptify_variable(@max_visible_markers)},#{MappingObject.javascriptify_variable(@grid_size)},#{MappingObject.javascriptify_variable(@min_markers_per_cluster)},#{MappingObject.javascriptify_variable(@max_lines_per_info_box)})" + end + + private + def add_description(marker) + "addDescriptionToMarker(#{MappingObject.javascriptify_variable(marker)},#{MappingObject.javascriptify_variable(marker.options[:description] || marker.options[:title] || '')})" + end + end + + #Makes the link with the MGeoRSS extension by Mikel Maron (a bit modified though). It lets you overlay on top of Google Maps the items present in a RSS feed that has GeoRss data. This data can be either in W3C Geo vocabulary or in the GeoRss Simple format. See http://georss.org to know more about GeoRss. + class GeoRssOverlay + include MappingObject + attr_accessor :url, :proxy, :icon, :options + + #You can pass the following options: + #- :icon: An icon for the items of the feed. Defaults to the classic red balloon icon. + #- :proxy: An URL on your server where fetching the RSS feed will be taken care of. + #- :list_div: In case you want a list of all the markers, with a link on which you can click in order to display the info on the marker, use this option to indicate the ID of the div (that you must place yourself). + #- :list_item_class: class of the DIV containing each item of the list. Ignored if option :list_div is not set. + #- :limit: Maximum number of items to display on the map. + #- :content_div: Instead of having an info window appear, indicates the ID of the DIV where this info should be displayed. + def initialize(url, options = {}) + @url = url + @icon = options.delete(:icon) || GIcon::DEFAULT + @proxy = options.delete(:proxy) || Variable::UNDEFINED + @options = options + end + + def create + "new GeoRssOverlay(#{MappingObject.javascriptify_variable(@url)},#{MappingObject.javascriptify_variable(@icon)},#{MappingObject.javascriptify_variable(@proxy)},#{MappingObject.javascriptify_variable(@options)})" + end + end + + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb new file mode 100644 index 0000000..bcde4e2 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb @@ -0,0 +1,34 @@ +module Ym4r + module GmPlugin + #A point in pixel coordinates + class GPoint < Struct.new(:x,:y) + include MappingObject + def create + "new GPoint(#{x},#{y})" + end + end + #A rectangular that contains all the pixel points passed as arguments + class GBounds + include MappingObject + attr_accessor :points + #Accepts both an array of GPoint and an array of 2-element arrays + def initialize(points) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GPoint.new(pt[0],pt[1]) } + else + @points = points + end + end + def create + "new GBounds([#{@points.map { |pt| pt.to_javascript}.join(",")}])" + end + end + #A size object, in pixel space + class GSize < Struct.new(:width,:height) + include MappingObject + def create + "new GSize(#{width},#{height})" + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb b/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb new file mode 100644 index 0000000..24c9068 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb @@ -0,0 +1,11 @@ +require 'gm_plugin/key' +require 'gm_plugin/mapping' +require 'gm_plugin/map' +require 'gm_plugin/control' +require 'gm_plugin/point' +require 'gm_plugin/overlay' +require 'gm_plugin/layer' +require 'gm_plugin/helper' +require 'gm_plugin/geocoding' + +include Ym4r::GmPlugin diff --git a/vendor/plugins/ym4r_gm/rakefile.rb b/vendor/plugins/ym4r_gm/rakefile.rb new file mode 100644 index 0000000..e2d46bf --- /dev/null +++ b/vendor/plugins/ym4r_gm/rakefile.rb @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the gm plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the gm plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'ym4r_gm-doc' + rdoc.title = 'GM' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake b/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake new file mode 100644 index 0000000..40745d0 --- /dev/null +++ b/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :gm do +# # Task goes here +# end \ No newline at end of file diff --git a/vendor/plugins/ym4r_gm/test/gm_test.rb b/vendor/plugins/ym4r_gm/test/gm_test.rb new file mode 100644 index 0000000..cea8447 --- /dev/null +++ b/vendor/plugins/ym4r_gm/test/gm_test.rb @@ -0,0 +1,79 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment") + + +require 'ym4r_gm' +require 'test/unit' + +include Ym4r::GmPlugin + +class TestGoogleMaps< Test::Unit::TestCase + def test_javascriptify_method + assert_equal("addOverlayToHello",MappingObject::javascriptify_method("add_overlay_to_hello")) + end + + def test_javascriptify_variable_mapping_object + map = GMap.new("div") + assert_equal(map.to_javascript,MappingObject::javascriptify_variable(map)) + end + + def test_javascriptify_variable_numeric + assert_equal("123.4",MappingObject::javascriptify_variable(123.4)) + end + + def test_javascriptify_variable_array + map = GMap.new("div") + assert_equal("[123.4,#{map.to_javascript},[123.4,#{map.to_javascript}]]",MappingObject::javascriptify_variable([123.4,map,[123.4,map]])) + end + + def test_javascriptify_variable_hash + map = GMap.new("div") + test_str = MappingObject::javascriptify_variable("hello" => map, "chopotopoto" => [123.55,map]) + assert("{hello : #{map.to_javascript},chopotopoto : [123.55,#{map.to_javascript}]}" == test_str || "{chopotopoto : [123.55,#{map.to_javascript}],hello : #{map.to_javascript}}" == test_str) + end + + def test_method_call_on_mapping_object + map = GMap.new("div","map") + assert_equal("map.addHello(123.4);",map.add_hello(123.4).to_s) + end + + def test_nested_calls_on_mapping_object + gmap = GMap.new("div","map") + assert_equal("map.addHello(map.hoYoYo(123.4),map);",gmap.add_hello(gmap.ho_yo_yo(123.4),gmap).to_s) + end + + def test_declare_variable_latlng + point = GLatLng.new([123.4,123.6]) + assert_equal("var point = new GLatLng(123.4,123.6);",point.declare("point")) + assert_equal("point",point.variable) + end + + def test_array_indexing + obj = Variable.new("obj") + assert_equal("obj[0]",obj[0].variable) + end + + def test_google_maps_geocoding + + + placemarks = Geocoding.get("Rue Clovis Paris") + assert_equal(Geocoding::GEO_SUCCESS,placemarks.status) + assert_equal(1,placemarks.length) + placemark = placemarks[0] + assert_equal("FR",placemark.country_code) + assert_equal("Paris",placemark.locality) + assert_equal("75005",placemark.postal_code) + + #test iwht multiple placemarks + placemarks = Geocoding.get('hoogstraat, nl') + assert_equal(Geocoding::GEO_SUCCESS,placemarks.status) + assert(placemarks.length > 1) + assert(placemarks[0].latitude != placemarks[1].latitude ) + + + end + + +end +