// -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- // vim:ts=2:sw=2:tw=80:et // $Id: NetHugz.java,v 1.2 2004/02/01 07:51:43 uhtu Exp $ /* * Copyright (c) 2004, Hugh Kennedy * All rights reserved. * * 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. * 3. Neither the name of the WiGLE.net nor Mimezine nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 * COPYRIGHT OWNER 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. */ package org.mimezine.codec.hugz; import org.mimezine.geo.GPSPoint; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; /** * A Codec module for .hugz WiGLE stumble files. * another drop in the vast bucket of wireless standards, * focused on publishing network density/location/time info * without disclosing network xSSIDs and demonstrating "negative * space in stumbles. */ public class NetHugz { /* a conditional-based implementation of the load and store methods ran similarly fast, and required 30% more code. */ /* public static void main(String argv[]) throws Exception { NetHugz n = new NetHugz(); Date lstart = new Date(); n.loadFromStream( new java.io.FileInputStream( argv[0] ) ); Date lend = new Date(); List l = n.getSamples(); System.out.println(l.size()); Date sstart = new Date(); n.saveToStream( new java.io.FileOutputStream(argv[1]), true ); Date send = new Date(); System.out.println("load took:"+(lend.getTime()-lstart.getTime())+"ms"); System.out.println("store took:"+(send.getTime()-sstart.getTime())+"ms"); } */ /** list of Samples */ private List samples; /** Map of headers. Strings keyed by Strings */ private Map headers; /** default header */ private Map default_headers; /** CVS revisions look like $Revision: 1.2 $ */ private static final String CVS_VERSION = "$Revision: 1.2 $"; /** the version of this component */ public static final String VERSION = CVS_VERSION.substring( CVS_VERSION.indexOf(':') + 2, CVS_VERSION.lastIndexOf('$') ); /** the network types */ public static final String TYPE_HEADER = "Type"; /** the .hugz file version */ public static final String VERSION_HEADER = "Version"; /** the creator program of this .hugz file */ public static final String CREATOR_HEADER = "Creator"; /** the sample interval in milliseconds */ public static final String INTERVAL_HEADER = "Interval"; /** our default .hugz file version */ public static final String DEFAULT_VERSION = "0.0.8"; /** our default creator */ public static final String DEFAULT_CREATOR = "WiGLE "+VERSION; /** our default type */ public static final String DEFAULT_TYPE = "801.11* Hug"; /** our default interval, in milliseconds */ public static final String DEFAULT_INTERVAL = "5000"; /** start a comment with: */ private static final String COMMENT_LEAD = "#"; /** record delimiter */ private static final String DELIMITER = "~"; /** time column header */ private static final String TIME_COLUMN = "Time"; /** latitude column header */ private static final String LAT_COLUMN = "Lat"; /** longitude column header */ private static final String LON_COLUMN = "Lon"; /** new network column header */ private static final String NEW_COLUMN = "RawNewNets"; /** seen network column header */ private static final String RE_COLUMN = "RawReNets"; /** lost network column header */ private static final String LOST_COLUMN = "RawLostNets"; /** best SNR column header */ private static final String SNR_COLUMN = "bestSNR"; /** count column header */ private static final String COUNT_COLUMN = "Count"; /** the default column header order */ private static List column_order = Arrays.asList(new String[]{ TIME_COLUMN, LAT_COLUMN, LON_COLUMN, NEW_COLUMN, RE_COLUMN, LOST_COLUMN, SNR_COLUMN, COUNT_COLUMN }); /** the default header order */ private static List header_order = Arrays.asList(new String[]{TYPE_HEADER, CREATOR_HEADER, VERSION_HEADER, INTERVAL_HEADER}); /** * build a NetHugz */ public NetHugz() { samples = new ArrayList(); headers = new HashMap(); default_headers = new HashMap(); default_headers.put( INTERVAL_HEADER, DEFAULT_INTERVAL ); default_headers.put( TYPE_HEADER, DEFAULT_TYPE ); default_headers.put( CREATOR_HEADER, DEFAULT_CREATOR ); default_headers.put( VERSION_HEADER, DEFAULT_VERSION ); } /** * get a header value. * * @param header the header value to get * @return the header value or null if not present */ public String getHeader(String header) { return (String)headers.get(header); } /** * set a header value. * * @param header the header name to set * @param value the header value */ public void setHeader(String header, String value) { headers.put(header, value); } /** * get a view of the data * @return an immutable view of the samples */ public List getSamples() { return Collections.unmodifiableList( samples ); } /** * load additional samples from in. appends to the current set. updates the observed headers. * * @param in the stream to load from * @return count of samples loaded from in * @throws IOException if in Excepts */ public int loadFromStream( InputStream in ) throws IOException { BufferedReader br = new BufferedReader( new InputStreamReader(in, "ISO-8859-1") ); int count = 0; String line = null; // get the headers, if any boolean readingHeader = true; while ( readingHeader ) { line = br.readLine(); if ( ! line.startsWith( COMMENT_LEAD ) ) { readingHeader = false; } else { // #Header value line = line.substring( COMMENT_LEAD.length() ); int offset = line.indexOf(" "); if ( offset < 0 ) { continue; } String head = line.substring(0, offset); String val = line.substring(offset + 1); headers.put( head, val); } } // line is set already.. // get the column header // this can be replaced with the one-liner noted below, for 1.4 and up StringTokenizer st = new StringTokenizer(line, DELIMITER); String[] colarray = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++){ colarray[i] = st.nextToken(); } List columns = Arrays.asList(colarray); // one-line 1.4 solution for the above block: // List columns = Arrays.asList( line.split( DELIMITER ) ); // XXX: bound? // get the Samples while ( ( line = br.readLine() ) != null ) { if ( line.startsWith( COMMENT_LEAD ) ) { continue; } st = new StringTokenizer(line, DELIMITER); if ( st.countTokens() != columns.size() ) { // bad line continue; } SubSample ss = new SubSample(); for ( Iterator i = columns.iterator(); i.hasNext(); ) { ((InsertColumnOperator)inbound_column_operations.get(i.next())).doit( st.nextToken(), ss ); } Sample s = new Sample( ss.time, new GPSPoint(ss.lat, ss.lon), ss.newNets, ss.reNets, ss.lostNets, ss.bestSNR, ss.count ); samples.add( s ); count++; } return count; } /** * save current samples to out. if header is true, writes out the * current set of headers, and the column headers. * * @param out the stream to save to, will not be closed except on exception * @param header write the header first? * @return did the samples save successfully? * @throws IOException if in Excepts */ public boolean saveToStream( OutputStream out, boolean header ) throws IOException { PrintWriter o = new PrintWriter( out ); if ( header ) { if ( headers.size() <= 0 ) { headers = new HashMap( default_headers ); } // dump the header out for (Iterator i = header_order.iterator(); i.hasNext(); ){ String h = (String) i.next(); String val = (String) headers.get(h); if ( null != val ) { o.println( COMMENT_LEAD + h + " " + val ); } } // assemble the column header String column_header = ""; for ( Iterator i = column_order.iterator(); i.hasNext(); ) { column_header += (String)i.next(); if ( i.hasNext() ) { column_header += DELIMITER; } } o.println(column_header); } for ( Iterator i = samples.iterator(); i.hasNext(); ) { o.println( columnOrdered( (Sample)i.next() ) ); } o.flush(); return true; } /** * prepare a Sample for logging. * @return the fields of the Sample in column_order, delimited by DELIMITER */ public String columnOrdered( Sample s ) { StringBuffer buf = new StringBuffer(); for ( Iterator i = column_order.iterator(); i.hasNext(); ) { String col = (String)i.next(); // this will NPE if we're missing an operator, which is a bigger deal, anyhow. ((ColumnOperator) column_operations.get(col)).doit( buf, s ); if ( i.hasNext() ) { buf.append( DELIMITER ); } } return buf.toString(); } /** operators for columnOrdered() extract data from Samples based on the .hugz column */ private static Map column_operations = new HashMap(); static { column_operations.put( TIME_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( ( s.time.getTime() / 1000 ) ); } } ); column_operations.put( LAT_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.loc.getLat() ); } } ); column_operations.put( LON_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.loc.getLon() ); } } ); column_operations.put( NEW_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.newNets ); } } ); column_operations.put( RE_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.reNets ); } } ); column_operations.put( LOST_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.lostNets ); } } ); column_operations.put( SNR_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.bestSNR ); } } ); column_operations.put( COUNT_COLUMN, new ColumnOperator() { public void doit(StringBuffer b, Sample s){ b.append( s.count ); } } ); } /** table-driven sample-data-extraction */ static abstract class ColumnOperator { /** * put data from s into b * @param b the buffer to append into * @param s the Sample to get data from */ public abstract void doit( StringBuffer b, Sample s ); } /** operators for inbound data*/ private static Map inbound_column_operations = new HashMap(); static { inbound_column_operations.put( TIME_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.time = new Date( Integer.parseInt(s) * 1000 ); } } ); inbound_column_operations.put( LAT_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.lat = Float.parseFloat(s); } } ); inbound_column_operations.put( LON_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.lon = Float.parseFloat(s); } } ); inbound_column_operations.put( NEW_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.newNets = Integer.parseInt(s); } } ); inbound_column_operations.put( RE_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.reNets = Integer.parseInt(s); } } ); inbound_column_operations.put( LOST_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.lostNets = Integer.parseInt(s); } } ); inbound_column_operations.put( SNR_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.bestSNR = Integer.parseInt(s); } } ); inbound_column_operations.put( COUNT_COLUMN, new InsertColumnOperator() { public void doit(String s, SubSample ss){ ss.count = Integer.parseInt(s); } } ); } /** table-driven sample-data-insertion */ static abstract class InsertColumnOperator { /** * put data from s into ss * @param s the String data * @param ss the SubSample to put the data into */ public abstract void doit( String s, SubSample ss ); } /** * a single entry in a .hugz file. * Samples are immutable, so all the members are public. */ public static class Sample { /* Time is in seconds since the unix epoch, GMT for this current sample Lat and Lon are in decimal degrees, and indicate the last seen location RawNewNets is the number of new networks seen in the current sample RawReNets is the number of networks that were seen in both the previous and current sample RawLostNets is the number of networks seen in the previous sample, not present in this one bestSNR is the best network SNR seen in the current sample Count is the number of underlying samples that are aggregated in the current sample */ /** when was this Sample taken? */ public final Date time; /** where was this Sample taken? */ public final GPSPoint loc; /** count of new networks in this Sample. >= 0. */ public final int newNets; /** count of networks in this and the previous Sample. >= 0. */ public final int reNets; /** count of networks in the previous Sample, but not in this one. >= 0. */ public final int lostNets; /** the best network Signal to Noise Ratio seen in this Sample. */ public final int bestSNR; /** the number of underlying samples in this Sample. > 0. */ public final int count; /** * build a simple Sample, with a count of one and a time of now. * * @param loc where was this Sample taken? * @param newNets count of new networks in this Sample. >= 0. * @param reNets count of networks in this and the previous Sample. >= 0. * @param lostNets count of networks in the previous Sample, but not in this one. >= 0. * @param bestSNR the best network Signal to Noise Ratio seen in this Sample. * @throws IllegalArgumentException if any of the arguments are null or violate their invariants. */ public Sample( GPSPoint loc, int newNets, int reNets, int lostNets, int bestSNR ) throws IllegalArgumentException { this( new Date(), loc, newNets, reNets, lostNets, bestSNR, 1); } /** * build a complete Sample * * @param time when was this Sample taken? * @param loc where was this Sample taken? * @param newNets count of new networks in this Sample. >= 0. * @param reNets count of networks in this and the previous Sample. >= 0. * @param lostNets count of networks in the previous Sample, but not in this one. >= 0. * @param bestSNR the best network Signal to Noise Ratio seen in this Sample. * @param count the number of underlying samples in this Sample. > 0. * @throws IllegalArgumentException if any of the arguments are null or violate their invariants. */ public Sample( Date time, GPSPoint loc, int newNets, int reNets, int lostNets, int bestSNR, int count ) throws IllegalArgumentException { if ( ( null == time ) || ( null == loc ) ) { throw new IllegalArgumentException("time and loc must not be null"); } if ( ( newNets < 0 ) || ( reNets < 0 ) || ( lostNets < 0 ) ) { throw new IllegalArgumentException("all network values must be >= 0"); } if ( count < 1 ) { throw new IllegalArgumentException("count must be >= 1"); } this.time = time; this.loc = loc; this.newNets = newNets; this.reNets = reNets; this.lostNets = lostNets; this.bestSNR = bestSNR; this.count = count; } public String toString() { return time + ": "+loc+" ("+newNets+","+reNets+","+lostNets+","+bestSNR+","+count+")"; } } /** * inner utility class for parsing */ static class SubSample { /* Time is in seconds since the unix epoch, GMT for this current sample Lat and Lon are in decimal degrees, and indicate the last seen location RawNewNets is the number of new networks seen in the current sample RawReNets is the number of networks that were seen in both the previous and current sample RawLostNets is the number of networks seen in the previous sample, not present in this one bestSNR is the best network SNR seen in the current sample Count is the number of underlying samples that are aggregated in the current sample */ /** when was this Sample taken? */ public Date time; /** where was this Sample taken? (lat) */ public float lat; /** where was this Sample taken? (lon) */ public float lon; /** count of new networks in this Sample. >= 0. */ public int newNets; /** count of networks in this and the previous Sample. >= 0. */ public int reNets; /** count of networks in the previous Sample, but not in this one. >= 0. */ public int lostNets; /** the best network Signal to Noise Ratio seen in this Sample. */ public int bestSNR; /** the number of underlying samples in this Sample. > 0. */ public int count; } }