Overview

WhereIs? is a preliminary implementation of an iphone utility that definately breaks your iphone terms of service but is a lot of fun. It tracks where you and your friends are by asking your iphone to publish its location every 10 minutes. This is displayed on a website as a tracklog of your motion through time. For me you can see where I am by visiting Where Is Anselm?. This is live all the time and updates all the time (every 10 minutes) forever. You can see a trace of my history at Where was Anselm?.

Installation

You will need to find the IP address of your iphone - goto your wifi settings and examine the current wifi network you are connected to. For me - right now - if I look at my IPhone it appears I am using 192.168.1.15 ... the one that you will find should be similar and you will want to use it instead of what I use in the examples. Now you are ready, let's go: 1) Copy finder and findme to the root of your iphone filesystem like so from your mac from a terminal shell window - like so: scp finder root@192.168.1.15:/finder scp findme root@192.168.1.15:/findme 2) Copy this file to here: scp com.apple.iphonehome.plist root@192.168.1.15:/System/Library/LaunchDaemons/com.apple.iphonehome.plist Done. If you are paranoid you may want to chmod +e your files. Also you may want to reboot your phone to make sure the launch daemon gets registered.

IPhone Side Code

The code itself consists of a few simple pieces. On the iphone client there is a very small application that has to be installed by hand for now - I used Erica Sadun's work for this but it is excessively trivial to write this from scratch if you want. This task wakes up every 10 minutes and sends a message about the phones location to a server. If you want to roll your own - see code from 'finder' or you can take a snippet of source code from say 'imagewiki' or 'iflickr' and bake your own client side app as well. Alternatively you can trivially do this now with built in support in the new official Apple IPhone Dev Docs. My code from imagewiki, itself from iflickr, looks like so:
-(void)initlocation
{
	
	[self cellConnect];
	[self getCellInfo:cellinfo];
	
	NSString *url=[NSString stringWithFormat:@"http://zonetag.research.yahooapis.com/services/rest/V1/cellLookup.php?apptoken=7107598df4d33d39bc70a6e8d5334e71&cellid=%d&lac=%d&mnc=%d&mcc=%d&compressed=1", cellinfo.cellid, cellinfo.location, cellinfo.network, cellinfo.servingmnc];
	
	NSLog(@"String is (%@)", url);
	
	NSURL *theURL = [NSURL URLWithString:url];
	
	NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:theURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:1000.0f];
	[theRequest setHTTPMethod:@"GET"];
	
	NSURLResponse *theResponse = NULL;
	NSError *theError = NULL;
	NSData *theResponseData = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:&theResponse error:&theError];
	NSString *theResponseString = [[NSString alloc] initWithData:theResponseData encoding:NSASCIIStringEncoding] ;
	NSLog(@"response  is (%@)", theResponseString);	
	
	int errcode = 0;
	id errmsg = nil;
	BOOL err = NO;
	
	NSXMLDocument *xmlDoc = [[NSClassFromString(@"NSXMLDocument") alloc] initWithXMLString:theResponseString options:NSXMLDocumentXMLKind error:&errmsg];
	NSXMLNode *stat =[[xmlDoc rootElement] attributeForName:@"stat"];
	NSLog(@"return (%@)\n", [stat stringValue]);
	
	if([[stat stringValue] isEqualToString:@"ok"])
	{
		NSArray *children = [[xmlDoc rootElement] children];
		int i, count = [children count];
		NSXMLElement *child = [children objectAtIndex:0];
		NSLog(@"Name (%@) : Value (%@) \n", [child name], [child stringValue]);
		location = [[NSString alloc]initWithString:[child stringValue]];
	}
	else
	{
		[alertSheet setBodyText:@"Could not get GSM location"];
		[alertSheet popupAlertAnimated:YES];
	}
	
}

-(void)getCellInfo:(struct CellInfo) cellinfo1;
{
	int cellcount;
		
	_CTServerConnectionCellMonitorGetCellCount(&tl,connection,&cellcount);
	NSLog(@"Cell count: %d (%d)\n",cellcount,tl);
	unsigned char *a=malloc(sizeof(struct CellInfo));
	for(i = 0; i < cellcount; i++)
	{
		_CTServerConnectionCellMonitorGetCellInfo(&tl,connection,i,a);
		
		memcpy(&cellinfo,a, sizeof(struct CellInfo)); 
		printf("Cell Site: %d, MCC: %d, ",i,cellinfo.servingmnc);
		printf("MNC: %d ",cellinfo.network);
		printf("Location: %d, Cell ID: %d, Station: %d, ",cellinfo.location, cellinfo.cellid, cellinfo.station);
		printf("Freq: %d, RxLevel: %d, ", cellinfo.freq, cellinfo.rxlevel);
		printf("C1: %d, C2: %d\n", cellinfo.c1, cellinfo.c2);
	}
	
	_CTServerConnectionCellMonitorGetCellInfo(&tl,connection,0,a);
				
	memcpy(&cellinfo,a, sizeof(struct CellInfo));
				
	if(a) free(a);
	
	return ;
}

-(void)cellConnect
{
        int tx;
        connection = _CTServerConnectionCreate(kCFAllocatorDefault, callback, NULL);

        CFMachPortContext  context = { 0, 0, NULL, NULL, NULL };

        ref=CFMachPortCreateWithPort(kCFAllocatorDefault, _CTServerConnectionGetPort(connection), sourcecallback, &context, NULL);

        _CTServerConnectionCellMonitorStart(&tx,connection);

       NSLog(@"Connected\n");

}
There is also a small launch daemon which goes in your iphone launch daemon folder:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">


        Label
        com.apple.iphonehome
        ProgramArguments
        
                /finder
        
        StartInterval
        600


And there is a small curl script that helps trigger this - this was also ripped off rom the 'finder' demo written by Erica Sadun - see IPhone Lojack Article.
curl --get --data status="`/findme`" http://hook.org/whereis[name]/

Server Side Code

The server side is also pretty simple. Incoming requests are caught by the index page itself - using an .rhtml file for which I've configured the Apache Webserver to handle in a fashion similar to .php pages. That configuration is beyond the scope of this article; but roughly speaking the script does something like this:
require 'cgi'
cgi = CGI.new
ll = cgi.params["status"].to_s
lll= "0,0"
url = ""
show = true
if (ll.length > 3) && ll  
  handle = File.new("ll","w")
  handle.puts(ll)
  handle.close
  show = false
  handle = File.new(Time.new.to_i.to_s,"w")
  handle.puts(ll)
  handle.close
else
  handle = File.new("ll","r")
  ll = handle.gets
  i = ll.index('http')
  url = ll[i..-1]
  i = ll.index('?ll')+4
  lll = CGI::unescape(ll[i..-1]) if i > 0
  handle.close
end
And for the fancy line drawing mode ( see lines.rhtml ) there is a variation on this theme with a little bit of extra support - stitching in a line rendering optimization routine and calling the google line drawing code in an efficient way. No mapserver or geoserver needed because this is actually pretty optimal thanks to the folks at google.
First we have a bit of embedded ruby code that fetches the set of points that are being stored in our current folder:

// ruby

require 'points.rb'
require 'encoder.rb'
Dir.chdir("/www/sites/hook.org/whereisanselm")
points = [[45,-122],[45,-20]];
points = points_load
points_save_xml points
encoder = GMapPolylineEncoder.new()
result = encoder.encode( points )
Then we have a bit of embedded javascript code that prints it
var map = new GMap2(document.getElementById("map"));
map.setCenter(new GLatLng(42.366662,-121.106262), 4);
map.addControl(new GMapTypeControl());
map.addControl(new GLargeMapControl());

var line = new GPolyline.fromEncoded({
                  color: "#FF0000",
                  weight: 3,
                  opacity: 0.5,
                  zoomFactor: <%=result[:zoomFactor]%>,
                  numLevels: <%=result[:numLevels]%>,
                  points: "<%=result[:points]%>",
                  levels: "<%=result[:levels]%>"
               });

map.addOverlay(line);
The ruby support code is kept separate. I have two utility files - one called 'points.rb' which is here:

require 'find'

def points_optimize()
  # write this - TODO
end

def points_load()
  points = []
  Find.find("./") do |path|
    if FileTest.directory?(path)
      next
    end
    f = File.new(path,"r")
    d = f.read
    begin
      terms = d.split(' ')
      lat = terms[5].split(',')[0].to_f
      lon = terms[6].to_f
      if lat && lat != 0.0 && lon && lon != 0.0
        if lat < 60 && lat > 0 && lon > -140 && lon < -20
          points << [lat,lon]
        end
      end
    rescue
    end
    f.close
  end
  return points
end

def points_dump(points)
  puts "var data = [\n"
  (0..points.length).step(1) do |i|
    lat = points[i][0]
    lon = points[i][1]
    puts "\t#{lat},#{lon},\n"
  end
  puts "];\n"
end

def points_save_xml(points)
  begin
    o = File.new("data.xml","w")
    o.write "\n"
    o.write "\n"
    points.each {|p| 
      o.write "\t\n" 
    }
    o.write "\n"
    o.close
  rescue
  end
end
And one from a third party here :

#
# Utility for creating Google Maps Encoded GPolylines
#
# License: You may distribute this code under the same terms as Ruby itself
#
# Author: Joel Rosenberg
#
# ( Drawing from the official example pages as well as Mark McClure's work )
#
# == Example
#
#   data = [
#     [ 37.4419, -122.1419],
#     [ 37.4519, -122.1519],
#     [ 37.4619, -122.1819],
#   ]
#
#   encoder = GMapPolylineEncoder.new()
#   result = encoder.encode( data )
#
#   javascript << "  var myLine = new GPolyline.fromEncoded({\n"
#   javascript << "     color: \"#FF0000\",\n"
#   javascript << "     weight: 10,\n"
#   javascript << "     opacity: 0.5,\n"
#   javascript << "     zoomFactor: #{result[:zoomFactor]},\n"
#   javascript << "     numLevels: #{result[:numLevels]},\n"
#   javascript << "     points: \"#{result[:points]}\",\n"
#   javascript << "     levels: \"#{result[:levels]}\"\n"
#   javascript << "  });"
#
# == Methods
#
#   Constructor args (all optional): 
#     :numLevels (default 18)
#     :zoomFactor (default 2)
#     :reduce: Reduce points (default true)
#     :escape: Escape backslashes (default true)
#
#   encode( points ) method
#     points (required): array of longitude, latitude pairs
#
#     returns hash with keys :points, :levels, :zoomFactor, :numLevels
#
# == Background
#
# Description: http://www.google.com/apis/maps/documentation/#Encoded_Polylines
# API: http://www.google.com/apis/maps/documentation/reference.html#GPolyline
# Hints: http://www.google.com/apis/maps/documentation/polylinealgorithm.html
#
# Example Javascript for instantiating an encoded polyline:
# var encodedPolyline = new GPolyline.fromEncoded({
#     color: "#FF0000",
#     weight: 10,
#     points: "yzocFzynhVq}@n}@o}@nzD",
#     levels: "BBB",
#     zoomFactor: 32,
#     numLevels: 4
# });
#
# == Changes
#
# 08.14.2007 - Release 0.2
#            Doug Fales pointed out a null pointer exception bug in the zoom
#            factor implementation
#
# 06.29.2007 - Release 0.1
#            Profiling showed that distance() accounted for 50% of the time when
#            processing McClure's British coast data. By moving the distance
#            calculation into encode(), we can cache a few of the calculations
#            (magnitude) and eliminate the overhead of the function call. This
#            reduced the time to encode by ~ 30%
#
# 06.21.2007 Implementing the Doublas-Peucker algorithm for removing superflous
#            points as per Mark McClure's design:
#                http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/
#
# 10.14.2006 Cleaned up (and finally grasped) zoom levels
#
# 09.2006    First port of the official example's javascript. Ignoring zoom
#            levels for now, showing points at all zoom levels
#
#++

class GMapPolylineEncoder
  attr_accessor :reduce, :escape #zoomFactor and numLevels need side effects
  attr_reader :zoomFactor, :numLevels
  
  # The minimum distance from the line that a point must exceed to avoid
  # elimination under the DP Algorithm.
  @@dp_threshold = 0.00001
  
  def initialize(options = {})
    # There are no required parameters

    # Nice defaults
    @numLevels  = options.has_key?(:numLevels)  ? options[:numLevels]  : 18
    @zoomFactor = options.has_key?(:zoomFactor) ? options[:zoomFactor] : 2
    
    # Calculate the distance thresholds for each zoom level
    calculate_zoom_breaks()
    
    # By default we'll simplify the polyline unless told otherwise
    @reduce = ! options.has_key?(:reduce) ? true : options[:reduce]
    
    # Escape by default; most people are using this in a web context
    @escape = ! options.has_key?(:escape) ? true : options[:escape]
    
  end
  
  def numLevels=( new_num_levels )
    @numLevels = new_num_levels
    # We need to recalculate our zoom breaks
    calculate_zoom_breaks()
  end
  
  def zoomFactor=( new_zoom_factor )
    @zoomFactor = new_zoom_factor
    # We need to recalculate our zoom breaks
    calculate_zoom_breaks()
  end
  
  def encode( points )
  
    #
    # This is an implementation of the Douglas-Peucker algorithm for simplifying
    # a line. You can thing of it as an elimination of points that do not
    # deviate enough from a vector. That threshold for point elimination is in
    # @@dp_threshold. See
    #
    #   http://everything2.com/index.pl?node_id=859282
    #
    # for an explanation of the algorithm
    #
    
    max_dist = 0  # Greatest distance we measured during the run
    stack = []
    distances = Array.new(points.size)
  
    if(points.length > 2)
      stack << [0, points.size-1]
      
      while(stack.length > 0) 
        current_line = stack.pop()
        p1_idx = current_line[0]
        pn_idx = current_line[1]
        pb_dist = 0
        pb_idx = nil
        
        x1 = points[p1_idx][0]
        y1 = points[p1_idx][1]
        x2 = points[pn_idx][0]
        y2 = points[pn_idx][1]
        
        # Caching the line's magnitude for performance
        magnitude = Math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
        magnitude_squared = magnitude ** 2
        
        # Find the farthest point and its distance from the line between our pair
        for i in (p1_idx+1)..(pn_idx-1)
        
          # Refactoring distance computation inline for performance
          #current_distance = compute_distance(points[i], points[p1_idx], points[pn_idx])
          
          # 
          # This uses Euclidian geometry. It shouldn't be that big of a deal since
          # we're using it as a rough comparison for line elimination and zoom
          # calculation.
          #
          # TODO: Implement Haversine functions which would probably bring this to
          #       a snail's pace (ehhhh)
          #
    
          px = points[i][0]
          py = points[i][1]
         
          current_distance = nil
          
          if( magnitude == 0 )
            # The line is really just a point
            current_distance = Math.sqrt((x2-px)**2 + (y2-py)**2)
          else
         
            u = (((px - x1) * (x2 - x1)) + ((py - y1) * (y2 - y1))) / magnitude_squared
            
            if( u <= 0 || u > 1 )
                # The point is closest to an endpoint. Find out which one
                ix = Math.sqrt((x1 - px)**2 + (y1 - py)**2)
                iy = Math.sqrt((x2 - px)**2 + (y2 - py)**2)
                if( ix > iy )
                  current_distance = iy
                else
                  current_distance = ix
                end
            else
                # The perpendicular point intersects the line
                ix = x1 + u * (x2 - x1)
                iy = y1 + u * (y2 - y1)
                current_distance = Math.sqrt((ix - px)**2 + (iy - py)**2)
            end
          end
          
          # See if this distance is the greatest for this segment so far
          if(current_distance > pb_dist)
            pb_dist = current_distance
            pb_idx = i
          end
        end
        
        # See if this is the greatest distance for all points
        if(pb_dist > max_dist)
          max_dist = pb_dist
        end
        
        if(pb_dist > @@dp_threshold)
          # Our point, Pb, that had the greatest distance from the line, is also
          # greater than our threshold. Process again using Pb as a new 
          # start/end point. Record this distance - we'll use it later when
          # creating zoom values
          distances[pb_idx] = pb_dist
          stack << [p1_idx, pb_idx]
          stack << [pb_idx, pn_idx]
        end
        
      end
    end
    
    # Force line endpoints to be included (sloppy, but faster than checking for
    # endpoints in encode_points())
    distances[0] = max_dist
    distances[distances.length-1] = max_dist
  
    # Create Base64 encoded strings for our points and zoom levels
    points_enc = encode_points( points, distances)
    levels_enc = encode_levels( points, distances, max_dist)
    
    # Make points_enc an escaped string if desired.
    # We should escape the levels too, in case google pulls a switcheroo
    @escape && points_enc && points_enc.gsub!( /\\/, '\\\\\\\\' )
   
    
    # Returning a hash. Yes, I am a Perl programmer
    return {
      :points     => points_enc,
      :levels     => levels_enc,
      :zoomFactor => @zoomFactor,
      :numLevels  => @numLevels,
    }
    
  end
  
  private
  
  def calculate_zoom_breaks()
    # Calculate the distance thresholds for each zoom level
    @zoom_level_breaks = Array.new(@numLevels);
    
    for i in 0..(@numLevels-1)
      @zoom_level_breaks[i] = @@dp_threshold * (@zoomFactor ** ( @numLevels-i-1));
    end
    
    return
  end
  
  def encode_points( points, distances )
    encoded = ""
    
    plat = 0
    plon = 0

    #points.each do |point| # Gah, need the distances.
    for i in 0..(points.size() - 1)
      if(! @reduce || distances[i] != nil )
        point = points[i]
        late5 = (point[0] * 1e5).floor();
        lone5 = (point[1] * 1e5).floor();

        dlat = late5 - plat
        dlon = lone5 - plon

        plat = late5;
        plon = lone5;

        # I used to need this for some reason
        #encoded << encodeSignedNumber(Fixnum.induced_from(dlat)).to_s
        #encoded << encodeSignedNumber(Fixnum.induced_from(dlon)).to_s
        encoded << encodeSignedNumber(dlat).to_s
        encoded << encodeSignedNumber(dlon).to_s
      end
    end

    return encoded

  end
  
  def encode_levels( points, distances, max_dist )
    
    encoded = "";
    
    # Force startpoint
    encoded << encodeNumber(@numLevels - 1)
    
    if( points.size() > 2 )
      for i in 1..(points.size() - 2)
        distance = distances[i]
        if( ! @reduce || distance != nil)
          computed_level = 0
        
          while (distance and (distance < @zoom_level_breaks[computed_level])) do
            computed_level += 1
          end
          
          encoded << encodeNumber( @numLevels - computed_level - 1 )
        end
      end
    end
    
    # Force endpoint
    encoded << encodeNumber(@numLevels - 1)
    
    return encoded;
    
  end
 
  def compute_distance( point, lineStart, lineEnd )
  
    #
    # Note: This has been refactored to encode() inline for performance and 
    #       computation caching
    #
    
    px = point[0]
    py = point[1]
    x1 = lineStart[0]
    y1 = lineStart[1]
    x2 = lineEnd[0]
    y2 = lineEnd[1]
   
    distance = nil
   
    magnitude = Math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
    
    if( magnitude == 0 )
      return Math.sqrt((x2-px)**2 + (y2-py)**2)
    end
   
    u = (((px - x1) * (x2 - x1)) + ((py - y1) * (y2 - y1))) / (magnitude**2)
    
    if( u <= 0 || u > 1 )
        # The point is closest to an endpoint. Find out which
        ix = Math.sqrt((x1 - px)**2 + (y1 - py)**2)
        iy = Math.sqrt((x2 - px)**2 + (y2 - py)**2)
        if( ix > iy )
          distance = iy
        else
          distance = ix
        end
    else
        # The perpendicular point intersects the line
        ix = x1 + u * (x2 - x1)
        iy = y1 + u * (y2 - y1)
        distance = Math.sqrt((ix - px)**2 + (iy - py)**2)
    end
    
    return distance
  end
  
  def encodeSignedNumber(num)
    # Based on the official google example
    
    sgn_num = num << 1

    if( num < 0 )
        sgn_num = ~(sgn_num)
    end

    return encodeNumber(sgn_num)
  end

  def encodeNumber(num)
    # Based on the official google example
    
    encoded = "";

    while (num >= 0x20) do
        encoded << ((0x20 | (num & 0x1f)) + 63).chr;
        num = num >> 5;
    end

    encoded << (num + 63).chr;
    return encoded;
  end
  
end

Rendering in Processing

Brandon Martin-Anderson made a little Processing version of this also for us - which we're going to use to generate pretty images out of all this data for a show.

import processing.xml.*;
 
XMLElement xml;

float INFINITY=10000000;

void frame(PGraphics pg, float left,float bottom,float right,float top) {
  pg.scale( pg.width/(right-left), pg.height/(bottom-top) );
  pg.translate( -left, -top );
}

void frame(float left,float bottom,float right,float top) {
  frame( this.g, left, bottom, right, top );
}

void zoom( PGraphics pg, float x, float y, float sfactor ) {
  float hspan = pg.width/sfactor;
  float vspan = pg.height/sfactor;
  float left = x+(hspan/2);
  float right = x-(hspan/2);
  float top = y-(vspan/2);
  float bottom = y+(vspan/2);
  //println( "frame to " + left + " " + bottom + " " + right + " " + top );
  frame( pg, left, bottom, right, top );
}

void zoom( float x, float y, float sfactor ) {
  zoom( this.g, x, y, sfactor );
}

void fit(float left, float bottom, float right, float top) {
  //println( "from edges to " + left + " " + bottom + " " + right + " " + top );
 
  float hscale = float(width)/(right-left);
  float vscale = float(height)/(bottom-top);
  float minscale = min(hscale,vscale);
 
  zoom((left+right)/2.0, (top+bottom)/2.0, minscale);
}

void setup() {
  int my_width = 1000;
  int my_height = 1000;


  size(my_width,my_height);
  background(122);
  stroke(255);
  strokeWeight(0.001); //stroke weight in terms of geographical units
  smooth();


  xml = new XMLElement(this, "http://hook.org/whereisanselm/data.xml");
  XMLElement[] kids = xml.getChildren();
  float old_lat = 0;
  float old_lon = 0;
 
  //find bounding box to fit screen
  float left = INFINITY;
  float right = -INFINITY;
  float top = -INFINITY;
  float bottom = INFINITY;

  for (int i=0; i < kids.length; i++) {
    float lat = kids[i].getFloatAttribute("lat");
    float lon = kids[i].getFloatAttribute("lon");
    if(lat>top) top=lat;
    if(latright) right=lon;
  }
 
  println( left );
  println( bottom);
  println( right);
  println( top);
  fit(left,bottom,right,top);
  //fit(-124.5,45.4, -121, 45.8 );

 
  for (int i=0; i < kids.length; i++) {
    float lat = kids[i].getFloatAttribute("lat");
    float lon = kids[i].getFloatAttribute("lon");
    if( old_lat != 0) {
      line(old_lon,old_lat,lon,lat);
    }
    old_lat = lat;
    old_lon = lon;
  }

  save("everything.png");


}

void draw() {
  stroke(255);
  if(mousePressed) {
    line(mouseX, mouseY, pmouseX, pmouseY);
  }
}

Source and Parts

Here are the relevant source files and pieces that I used to put this together - you can take a shot at this and improve it if you wish - it is pretty easy.