#!/usr/bin/perl -w =head1 NAME owlps-aggsetcoord - set the coordinates in an aggregation CSV file =head1 SYNOPSIS B [ B<-h> | B<-V> ] [ B<-v> ] [ B<-i> | B<-I> I ] [ B<-c> I [ B<-l> I ] ] [ B<-m> I ] [ I ] =head1 DESCRIPTION B is a tool to set the coordinates of mobile devices in a CSV aggregation file, in an automated way. It works only with lines containing positioning requests; the lines containing other types of requests are left untouched. Furthermore, only the default values of the coordinates will be treated; a non-default coordinate will always be left untouched. Note that the default coordinate in OwlPS is registered in aggregation files as "0.00", therefore this program considers the string "0.00" as the default value corresponding to an undefined coordinate. Zero coordinates are not prevented, but you have to write them in another way in your input file, such as "0", "00.00", "0.0", "0.000", etc. By default, B uses the coordinates already present in the input file (manually set by the user, most of the time) to interpolate the coordinates of lines containing default values. A given coordinate is interpolated according to the previous and next known coordinates and their timestamps, assuming the mobile moves at a constant speed between two known coordinates. The interpolation is done in 3 dimensions, each dimension being interpolated independently. Alternatively, using the B<-c> option will disable interpolation but apply an arbitrary set of coordinates to the lines containing only default values. In theory, you can provide more than one I at once, but don't do that unless you really know what you're doing; you cannot specify multiple input files when using the B<-i> option. The (list of) aggregation file name(s) I be placed after the options. If no file is provided, the standard input is read. Unless B<-i> is used, the results are written on the standard output. It is advised to use graphical diff tools such as I to visualise the differences between the input file and the result and make sure it corresponds to what was expected. =head1 OPTIONS =over 7 =item B<-h>, B<--help> Print version and help message and exit. =item B<-V>, B<--version> Print version message and exit. =item B<-v> Turn on verbose mode (displays a trace on the standard error). =item B<-i> Modify the input file in-place. The original file is saved with suffix ".orig". =item B<-I> I Same as B<-i>, but the original file is suffixed with I instead of ".orig". If both B<-i> and B<-I> are used, the latter has precedence over the former. If I is an empty string, this option is ignored. =item B<-c> I Apply I to every positioning request with default X, Y and Z. A line in which at least one of the three coordinates is set to a non-default value will be left untouched. I is a string "X;Y;Z". =item B<-l> I Skip lines that are not part of the selection pattern I, which is evaluated as a Perl numeric list. For example, "3, 5, 8..10" will select the lines 3, 5, 8, 9 and 10; "1..25,42..45" will select the lines 1 to 25 and 42 to 77. This works only with the B<-c> option. =item B<-m> I Work only on positioning requests transmitted by MAC address I. =back =head1 BATCH PROCESSING If you have a bunch of different coordinates to put in the same aggregation file (named F in the following example), you can prepare a file that contains, on each line, a line range and the set of coordinates to apply at this line range, separating the two fields by a space. You can then use a little shell script such as the following to read your input file (named F here) and call AggSetCoord for each line: FILE=input.agg BATCHFILE=batchfile.txt i=0 while read LINES COORD ; do i=$(expr $i + 1) echo "Setting coordinates ($COORD) at line(s) $LINES" owlps-aggsetcoord -I .step$i -l "$LINES" -c "$COORD" "$FILE" done <"$BATCHFILE" You can also include the MAC address in the input file and call AggSetCoord with the B<-m> option. =head1 COPYING This script and its documentation are part of the Owl Positioning System (OwlPS) project. They are subject to the copyright notice and license terms in the COPYRIGHT.t2t file found in the top-level directory of the OwlPS distribution and at https://code.lm7.fr/mcy/owlps/src/master/COPYRIGHT.t2t =head1 SEE ALSO owlps(7), owlps-aggregatord(1) =cut use strict; use Getopt::Std; use Pod::Usage; use File::Temp qw/tempfile/; use File::Basename qw/dirname/; use File::Copy qw/move/; use OwlPS::TimeInterpolation; use OwlPS::CSV; ## Constants ## # CSV format version handled by this program use constant FORMAT_VERSION => 1; # Indexes of the fields in the CSV lines (first field is #0) use constant MAC_FIELD_INDEX => 1; use constant TYPE_FIELD_INDEX => 2; use constant TIMESTAMP_FIELD_INDEX => 4; use constant X_FIELD_INDEX => 5; use constant Y_FIELD_INDEX => 6; use constant Z_FIELD_INDEX => 7; # Number corresponding to a normal positioning request use constant REQUEST_TYPE_NORMAL => 0; # String corresponding to an "empty" coordinate in the input file use constant BASE_COORD => "0.00"; ## Global variables ## # Output file handle my $output_handle = *STDOUT; # Names of the temporary and final output files my ($tmp_output_file, $output_file); # Suffix with which the original file is saved my $backup_suffix; # Verbose mode my $verbose; # List of the lines to work on (will stay undefined if all the lines have to be # worked on) my %selected_lines; # Selected transmitter's MAC address (will stay undefined if -m was not used) my $selected_mac; # New coordinates to apply to the selected lines (will stay undefined if -c was # not used) my $new_coordinates; my ($new_x, $new_y, $new_z); # Current line my $cur_line; # Current line number my $line_nb = 0; # Last "real" values of X, Y and Z to interpolate with my ($last_x, $last_y, $last_z); # Timestamps corresponding to each of these last values my ($last_x_t, $last_y_t, $last_z_t); # Number of waiting Xs, Ys and Zs that we will have to interpolate my ($xs, $ys, $zs) = (0, 0, 0); # Array of the previous lines to interpolate (we store them while we are waiting # for the next "complete" line) my @prev_lines; ## Functions ## sub VERSION_MESSAGE { my $handle = $_[0] || *STDOUT; print $handle "This is OwlPS AggSetCoord, part of the Owl Positioning System project.\n" . "CSV aggregation format version handled: " . FORMAT_VERSION . "\n"; } sub HELP_MESSAGE { my $handle = $_[0] || *STDOUT; pod2usage(-output => $handle, -exitval => "NOEXIT"); } # Prints a trace message if verbose mode is active (no new line is printed). # Parameters: @message sub trace(@) { print STDERR "Line #$line_nb: @_" if ($verbose); } # Prints a warning message terminated by a new line. # Parameters: @message sub print_warning(@) { print STDERR "Warning on line #$line_nb: @_\n"; } # Prints the arguments to the output handle. sub output(@) { print $output_handle @_; } # Returns true if $mac is the transmitter's MAC address selected by the user, or # if the user didn't select any particular MAC address. # Parameters: $mac sub is_selected_mac($) { if (!defined($selected_mac)) { return 1 } return $selected_mac eq $_[0]; } # Returns true if all $x, $y and $z are ready to be printed. # Arguments: $x, $y, $z sub is_complete($$$) { my ($x, $y, $z) = @_; return ($x ne BASE_COORD && $y ne BASE_COORD && $z ne BASE_COORD); } # Returns true if all $x, $y and $z are untouched (fully incomplete line) or # undefined. # Arguments: $x, $y, $z sub is_blank($$$) { my ($x, $y, $z) = @_; return (!defined($x) && !defined($y) && !defined($z)) || ($x eq BASE_COORD && $y eq BASE_COORD && $z eq BASE_COORD); } # Checks the stored previous line and prints/delete what the complete lines, # starting with the oldest line stored and stopping with the first incomplete # line. sub check_oldest_lines() { while ($prev_lines[0]) { # If the oldest stored line is empty, just print it and delete it if ($prev_lines[0] eq "\n") { output(shift @prev_lines); next; } # Check the oldest (non-empty) line my ( $csv_format, $mac, $type, $nb_pkt, $timestamp, $x, $y, $z, $end_line ) = split(';', $prev_lines[0], 9); # Display and delete the line if either of the following is true: # - it is not a positioning request # - it is not the selected transmitter's MAC address # - it is complete if ( $type != REQUEST_TYPE_NORMAL || !is_selected_mac($mac) || is_complete($x, $y, $z)) { output(shift @prev_lines); next; # Jump to the next oldest line } # If this line is not complete, it will have to be interpolated and the # potential newer stored lines as well, so we end the check here last; } } # Modifies the field number $idx of $nb_lines previous lines stored in # @prev_lines with values interpolated from real coordinates $start and $stop # registered at $start_time and $stop_time. Lines containing non-positioning # requests are skipped and do not count in $nb_lines. Parameters: $nb_lines, # $idx, $start, $start_time, $stop, $stop_time sub apply_interpolation($$$$$$) { my ($nb_lines, $idx, $start, $start_time, $stop, $stop_time) = @_; for (my $i = 1 ; $i <= $nb_lines ; $i++) { # Skip blank lines if ($prev_lines[-$i] eq "\n") { # Incrementing the number of lines to treat has no incidence but to # allow the loop to go one element further to take into account the # blank line $nb_lines++; next; } # Skip non-positioning requests my $type = get_field($prev_lines[-$i], TYPE_FIELD_INDEX); if ($type != REQUEST_TYPE_NORMAL) { $nb_lines++; next; } # Skip requests sent by other devices than the selected one if (defined($selected_mac)) { my $mac = get_field($prev_lines[-$i], MAC_FIELD_INDEX); if ($selected_mac ne $mac) { $nb_lines++; next; } } my $time = get_field($prev_lines[-$i], TIMESTAMP_FIELD_INDEX); my $newvalue = interpolate_1d($start, $start_time, $stop, $stop_time, $time); # Round the interpolated coordinate to two decimals $newvalue = round2($newvalue); # But if the new value is "0.00", it will be considered as "incomplete", # so we add a leading 0 to avoid being confused when we later check the # line $newvalue = "0" . $newvalue if ($newvalue == BASE_COORD); $prev_lines[-$i] = update_field($prev_lines[-$i], $idx, $newvalue); } } ## Option parsing ## $Getopt::Std::STANDARD_HELP_VERSION = 1; use constant OPTIONS => 'c:hiI:l:m:vV'; my %options; if (!getopts(OPTIONS, \%options)) { HELP_MESSAGE(*STDERR); exit 1; } if ($options{'h'}) { VERSION_MESSAGE(); HELP_MESSAGE(); exit 0; } if ($options{'V'}) { VERSION_MESSAGE(); exit 0; } $verbose = $options{'v'}; $new_coordinates = $options{'c'}; $backup_suffix = $options{'I'}; $selected_mac = $options{'m'}; if ($options{'i'} || $backup_suffix) { if (@ARGV != 1) { die "One and only one file name must be provided when using -i or -I"; } $backup_suffix ||= ".orig"; $output_file = $ARGV[0]; ($output_handle, $tmp_output_file) = tempfile( ".owlps-aggsetcoord-XXXXXXX", SUFFIX => ".agg", DIR => dirname($output_file), UNLINK => 1 ); } if (defined($new_coordinates)) { ($new_x, $new_y, $new_z) = split_point_3d($new_coordinates); } # Register the selected lines if (defined($options{'l'})) { die "-l option requires -c option!" if (!defined($new_coordinates)); $selected_lines{$_} = 1 foreach eval $options{'l'}; } ## Main loop: read lines ## while ($cur_line = <>) { $line_nb++; trace($cur_line); # The current line is empty if ($cur_line eq "\n") { # If we don't have previous lines, we print it; if we do, we store it if (@prev_lines == 0) { output("\n") } else { push @prev_lines, "\n" } next; } my ( $csv_format, $mac, $type, $nb_pkt, $timestamp, $x, $y, $z, $end_line ) = split(';', $cur_line, 9); # Check the CSV format version if ($csv_format != FORMAT_VERSION) { die "CSV format \"$csv_format\" not supported!"; } # Check the stored previous line and print what we can check_oldest_lines(); # Check the request type: we work only on positioning requests but must take # into account other request types if ($type != REQUEST_TYPE_NORMAL) { trace("Non-positioning request: line skipped.\n"); # If we have no lines in memory, we can just print the line and jump to # the next one, otherwise we have to store the line if (@prev_lines == 0) { output($cur_line) } else { push @prev_lines, $cur_line } next; } # Check the transmitter's MAC address if (!is_selected_mac($mac)) { trace( "Transmitter's MAC address ($mac) doesn't match the selected" . " transmiter's MAC address ($selected_mac): line skipped.\n"); # Print or store the line if (@prev_lines == 0) { output($cur_line) } else { push @prev_lines, $cur_line } next; } # The user specified a coordinate to apply to every pertinent line (-c # option): we print it immediately (we are sure not to have any previous # lines in memory, since we don't do that in this mode) if (defined($new_coordinates)) { # Skip the line if it is not part of the selected lines if (%selected_lines and !$selected_lines{$line_nb}) { trace("Line not selected: skipped.\n"); output($cur_line); next; } # If the current line is fully incomplete, we reconstitute it with the # new coordinates if (is_blank($x, $y, $z)) { output( $csv_format, ";", $mac, ";", $type, ";", $nb_pkt, ";", $timestamp, ";", $new_x, ";", $new_y, ";", $new_z, ";", $end_line ); } # If the current line has at least one of its coordinates to a # non-default value, just print it else { output($cur_line); } # In either case, jump to the next line next; } # No previous line in memory: we might have got a chance to get rid of the # current line right now if (@prev_lines == 0) { # The current line is "complete": print it and store the coordinates for # later interpolation if (is_complete($x, $y, $z)) { # Print current line output($cur_line); # Store current values $last_x = $x; $last_y = $y; $last_z = $z; # Store current timestamps $last_x_t = $last_y_t = $last_z_t = $timestamp; # Jump to next line next; } # The current line is fully incomplete (all the coordinates at 0) and we # don't have previous coordinates to interpolate: this should not happen # if the input file is correct, so we print a warning but don't store # the line since we won't be able to interpolate anything anyway if (is_blank($x, $y, $z) && is_blank($last_x, $last_y, $last_z)) { print_warning("No previous X, Y and Z values to interpolate!"); output($cur_line); next; } } # OK, the serious work starts here. The current line is either complete or # incomplete. If it is incomplete, it can still contain non-zero values, in # which case we will have to interpolate the previously stored values with # this value. For instance, if the current line have X = 0, Y = 42, Z = 0, # it is incomplete because X and Z are zero, but we will use the Y value # (42) as the end bound to interpolate Y values of the previous lines we # stored (assuming we have a begin bound value for Z). In this case, the # interpolation is done by modifying the lines in memory (which can still # have missing values even after the interpolation, but that will be checked # by the code above, at the next loop). # # In any case, if the line is incomplete, we will store it for later # interpolation (except if there are no real coordinates before this line, # but this should not happen if your input file is correct). # # If it is complete, we will also store it after having interpolated the # previous lines in memory, and all the lines will be printed at the next # loop in the check above (or after the end of the loop if it's the last # line). # Check if we have to compute X's value if ($x eq BASE_COORD) { if (!defined($last_x)) { # We've got a problem if we don't have a previous value to # interpolate print_warning("No previous X value to interpolate!"); } else { $xs++; } } # Check if we have to compute Y's value if ($y eq BASE_COORD) { if (!defined($last_y)) { print_warning("No previous Y value to interpolate!"); } else { $ys++; } } # Check if we have to compute Z's value if ($z eq BASE_COORD) { if (!defined($last_z)) { print_warning("No previous Z value to interpolate!"); } else { $zs++; } } # Interpolate what we can if needed if (@prev_lines > 0) { # X interpolation if ($xs > 0 && $x ne BASE_COORD) { apply_interpolation($xs, X_FIELD_INDEX, $last_x, $last_x_t, $x, $timestamp); $xs = 0; # There are no more Xs waiting } # Y interpolation if ($ys > 0 && $y ne BASE_COORD) { apply_interpolation($ys, Y_FIELD_INDEX, $last_y, $last_y_t, $y, $timestamp); $ys = 0; # There are no more Ys waiting } # Z interpolation if ($zs > 0 && $z ne BASE_COORD) { apply_interpolation($zs, Z_FIELD_INDEX, $last_z, $last_z_t, $z, $timestamp); $zs = 0; # There are no more Zs waiting } } # Store the current line; if it is complete, previous lines should be # complete as well now, since we just interpolated what needed to do, so all # the lines in @prev_lines will be printed at the check at the beginning of # the next loop trace("Line stored.\n"); push @prev_lines, $cur_line; # Store the current coordinates if they are non-zero if ($x ne BASE_COORD) { $last_x = $x; $last_x_t = $timestamp; } if ($y ne BASE_COORD) { $last_y = $y; $last_y_t = $timestamp; } if ($z ne BASE_COORD) { $last_z = $z; $last_z_t = $timestamp; } } ## Display the lines remaining in memory if any ## if (@prev_lines) { trace("Printing " . scalar(@prev_lines) . " lines left in memory...\n"); output($_) foreach (@prev_lines); } trace("The end.\n"); ## Write the output file if needed ## if (defined($output_file)) { # Close the output handle close $output_handle or die "Can't close the output file handle: $!"; # Backup the original file my $newname = $output_file . $backup_suffix; move($output_file, $newname) or die "Can't move \"$output_file\" to \"$newname\": $!"; # Move the temporary file to the new file move($tmp_output_file, $output_file) or die "Can't move the temporary file to \"$output_file\": $!"; } # vim: tabstop=4:shiftwidth=4:expandtab:textwidth=80