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.
- finder is a small curl script for the iphone that should be triggered every 10 minutes.
- findme is Erica's utility to get the location data
- com.apple.iphonehome.plist is a launch daemon that goes in your launch daemon folder wherever that is (usually in /System/Library/LaunchDaemons ? ).
- encoder.rb is Joel Rosenburg's nice little google line encoder for performance
- lines.rhtml is my little line drawing routine that wraps up some of the other routines
- points.rb is a small piece of code to fetch the line parts into a list - clearly a database would be more optimal but.
- index.rhtml is my index page - this isn't secure or anything so i guess anybody can add data to it if they want.