#!/usr/bin/perl ############################################################# # The Shodor Education Foundation # # Brian Block - 6/21/2006 # # v2j_make_form.cgi # # create form options for the vensim2java model # # Based on code written by Dave Joiner for Stella2Java # # # # Purpose: v2j_make_form.cgi is based on a completely # # revised parsing method with a more object oriented # # design. Ideally, this keeps things more organized and # # any future large-scale modifications will be simpler. # ############################################################# use CGI qw/:standard *table start_ul/; #load standard CGI routines use CGI::Pretty qw( :html3 ); #format html source so it isnt in 1 line use V2JHelperFunctions qw/:v2j/; #load v2j helper functions #contains the vensim documentation, as input by the user $user_input=param('user_input'); #issues with ^M in input. ^M is a carrage return, referred to by octal key \015 $user_input =~ tr/[\015]//; #the idea behind the OOD of this script is based on the fact #that in order to create the outfile that is sent to the applet-making #backend, the script has to have an understanding of the model and #how each element connects. s2j uses a series of arrays, but this #seemed to make more sense to me. # #there is the main container 'MODEL' which contains hashes of #the objects that make up the vensim model (stocks, flows, constants, #and aux variables) #######################################START_DEFINE_CLASSES #///////////////////////////MODEL CONTAINER package MODEL; sub new{ #constructor equivalent my($type) = shift; my($self) = { "stock" => {}, #initially empty "flow" => {}, # " " "constant" => {}, # " " "variable" => {} # " " }; our $stock_count; our $flow_count; our $var_count; our $constant_count; bless($self, $type); return($self); } sub print_model_contents{ #used to check that the vensim data is parsed properly and that #the MODEL is built correctly. strictly for debugging purposes. my($self) = shift; my($debug_file) = shift; #parsed_input.txt - used for debugging my ($key); my ($index) = 0; open (PARSEFILE, "> $debug_file"); #DO THIS TYPE OF THING FOR EVERY PROPERTY OF A NODE, AND #FOR EVERY OBJECT TYPE IN A SEPERATE FXN (hash traversals #are random, therefore the $objtype throws some things off). while (($objtype, $obj) = each %$self){ if ($objtype eq 'stock'){ print PARSEFILE "STOCKS:\n"; foreach $key (keys %$obj){ print PARSEFILE "$key = " . $self->{$objtype}->{$key}->{'value'} . " \(" . $self->{$objtype}->{$key}->{'units'} . "\) \n"; } print PARSEFILE "\n\n"; } elsif ($objtype eq 'constant'){ print PARSEFILE "CONSTANTS:\n"; foreach $key (keys %$obj){ print PARSEFILE "$key = " . $self->{$objtype}->{$key}->{'value'} . " \(" . $self->{$objtype}->{$key}->{'units'} . "\) \n"; } print PARSEFILE "\n\n"; } elsif ($objtype eq 'variable'){ print PARSEFILE "AUX VARIABLES:\n"; foreach $key (keys %$obj){ print PARSEFILE "$key = " . $self->{$objtype}->{$key}->{'value'} . " \(" . $self->{$objtype}->{$key}->{'units'} . "\) \n" . "is_lookup= " . $self->{$objtype}->{$key}->{'is_lookup'} . "\n"; } print PARSEFILE "\n\n"; } elsif ($objtype eq 'flow'){ print PARSEFILE "FLOWS:\n"; foreach $key (keys %$obj){ print PARSEFILE "$key = " . $self->{$objtype}->{$key}->{'value'} . " \(" . $self->{$objtype}->{$key}->{'units'} . "\) \n" . "flow_to: " . $self->{$objtype}->{$key}->{'flow_to'} . "\n" . "flow_from: " . $self->{$objtype}->{$key}->{'flow_from'} . "\n" . "is_lookup= " . $self->{$objtype}->{$key}->{'is_lookup'} . "\n"; } print PARSEFILE "\n\n"; } } close(PARSEFILE); } sub add_stock{ #the 'self' here is the MODEL object (passed automatically), whereas #the indexing key (stock name) is passed manually my($self) = shift; my($key) = shift; $self->{'stock'}->{$key} = STOCK->new(); $stock_count += 1; } sub add_flow{ my($self) = shift; my($key) = shift; $self->{'flow'}->{$key} = FLOW->new(); $flow_count += 1; } sub add_constant{ my($self) = shift; my($key) = shift; $self->{'constant'}->{$key} = VENSIM_VARIABLE->new(); $constant_count += 1; } sub add_vensim_variable{ #everything left over in this category is deemed an 'aux variable' my($self) = shift; my($key) = shift; $self->{'variable'}->{$key} = VENSIM_VARIABLE->new(); $var_count += 1; } #////////////////////////////////////////// #//////////////////////////PARENT CLASS #this is the main 'variable' class inherited by #each of the other classes. package VENSIM_VARIABLE; sub new{ #constructor my($type) = shift; #pops class name off @_ my($self) = { #stores class data in hash "units" => undef, "value" => undef, "is_lookup" => 0, #bool to see if var is a graph "is_ite" => 0, "ites" => [] }; #$self is a reference to a hash bless($self, $type); #the 'bless' command changes an object's type # in this case from HASH to VENSIM_VARIABLE return($self); } #///////////////////////////////////////// #///////////////////////////FLOW CLASS package FLOW; @ISA = (VENSIM_VARIABLE); #inherit methods from parent sub new{ my($type) = shift; my($self) = VENSIM_VARIABLE->new(); #inherit properties from parent #the flow_to/from keys are specific to flows #and point to where they flow to and what they flow #from (default = "NULL") $self->{"flow_to"} = "NULL"; $self->{"flow_from"} = "NULL"; bless($self, $type); return($self); } #///////////////////////////////////////// #///////////////////////////STOCK CLASS package STOCK; @ISA = (VENSIM_VARIABLE); sub new{ my($type) = shift; my($initial_value) = shift; #unique property to STOCKs my($self) = VENSIM_VARIABLE->new(); $self->{"initial_value"} = undef; bless($self, $type); return($self); } #////////////////////////////////////// package main; #END_DEFINE_CLASSES########################################## ###############################################START_DATA_PARSE $model = new MODEL; #container for all vensim objects #################temp file creation $tmp = "/tmp"; $tmp_num = "$$"; #gets a unique number $tmp_dir = "v2j$tmp_num"; #makes a tmp folder $template_path = "/home/httpd/cgi-bin/misctools/vensim2java/template"; $input_file = "$tmp/$tmp_dir/input.txt"; #contains the user $short_input_file = "$tmp/$tmp_dir/short_input.txt"; #used for the expand/collapse #option on the form page $parsed_input = "$tmp/$tmp_dir/parsed_input.txt"; #used for print_model_contents (debugging) system "mkdir $tmp/$tmp_dir"; system "chmod -R 755 $tmp/$tmp_dir"; system "rm $tmp/$tmp_dir/*"; system "cp $template_path/* $tmp/$tmp_dir"; open(TEMPFILE, "> $input_file"); print TEMPFILE $user_input; close(TEMPFILE); #end temp file creation############# open(INFILE, "$input_file") || die "$input_file not found"; @vensim_data = ""; $i=0; while($line = ){ #read input line by line chomp $line; $vensim_data[$i] = $line; $i+=1; } close(INFILE); #@vensim_data stores each line with original spacing/formatting (excluding # the chomped part) #sort through array and format multi-line strings into one line. then, using #different methods, sort the data into the appropriate object. $total_number_of_lines = @vensim_data; for ($i=0; $i<$total_number_of_lines; $i+=1){ $current_line = $vensim_data[$i]; if ($current_line =~ /^\(\d+\)/){ #line starts with: "()" #$current_line format status: original spacing - the only lines that #dont have leading whitespace are ones that start with an object id #with the format "(n)..." where n is the id number $current_line =~ s/^\(\d+\)//; #remove id #so now we know the current line starts with an object name (now that #the id has been removed. the string format currently has a leading space, #an unformated object name (vensim allows all kinds of things). after #another potential space, there's the "=" assignment operator. #the only object type (at least the only one i've seen so far) that #doesnt have an = sign is a named lookup. so check if the line doesnt #have an = sign, and if it doesnt, add one after valid name characters. if ($current_line !~ /\=/){ if ($current_line =~ /^([\w\s]*)/){ $testing = $1; $current_line =~ s/$testing/$testing\=/; } } #split line at the "=" sign to check validity of object name (left hand side) @split_on_equals = split("\=",$current_line); #now split_on_equals contains 2 entries - the first is the unformatted #object name, and the second is the rest of the line $potential_var = shift @split_on_equals; #pops off first element #now test and format the object name: if (!(&is_valid_object_name($potential_var))){ &html_error("$potential_var"); } $potential_var = &format_object_name($potential_var); #now we have a nicely formatted and readable object name. the next #step is to perform similar operations on the equation associated #with this object $current_object_value = shift @split_on_equals; ####EQUATION_BUILDING_START############################# #keep concating following lines, until you reach the line #that starts with 'Units' $current_object_value = &strip_whitespace($current_object_value); $next_line=$vensim_data[$i+=1]; #look at next line #while equation is still being read... while (!($next_line =~ /^\s*Units/i)){ $current_object_value = $current_object_value . &strip_whitespace($next_line); $next_line=($vensim_data[$i+=1]); if ($i > $total_number_of_lines){ #stuck in loop (bad user input) &html_loop_error(); } } #remove spaces around operators, but replace them with _ around #object names $current_object_value = &format_value_equation($current_object_value); #################################EQUATION_BUILDING_END### if( &IS_STOCK($current_object_value)){ my $stock_equation = &get_lone_value($current_object_value); my $stock_initial_value = &get_initial_value($current_object_value); my $stock_units = &get_units($next_line); $model->add_stock($potential_var); $model->{'stock'}->{$potential_var}->{'value'} = $stock_equation; $model->{'stock'}->{$potential_var}->{'initial_value'} = $stock_initial_value; $model->{'stock'}->{$potential_var}->{'units'} = $stock_units; } elsif( &IS_CONSTANT($current_object_value)){ my $constant_value = $current_object_value; my $constant_units = &get_units($next_line); $model->add_constant($potential_var); $model->{'constant'}->{$potential_var}->{'value'} = $constant_value; $model->{'constant'}->{$potential_var}->{'units'} = $constant_units; } else{ my $variable_value = $current_object_value; my $variable_units = &get_units($next_line); my $lookup_boolean = 0; #used to see if aux var is a vensim lookup table $model->add_vensim_variable($potential_var); $model->{'variable'}->{$potential_var}->{'value'} = $variable_value; $model->{'variable'}->{$potential_var}->{'units'} = $variable_units; $model->{'variable'}->{$potential_var}->{'is_lookup'} = &IS_LOOKUP($variable_value); $model->{'variable'}->{$potential_var}->{'is_ite'} = &IS_ITE($variable_value); #if a named lookup was found, push its name onto a stack. each aux #var and flow will be checked for instances of this name, which will #mean that those vars/flows are equivalent to a regular lookup. if ($model->{'variable'}->{$potential_var}->{'is_lookup'} == 2){ push @named_lookups, $potential_var; } } } } if (&get_number_of_objects($model) == 0){ &html_empty_error(); } #after named lookups are found, change the equations to the equivalent #of regular lookups (WITH_LOOKUP) $model = &replace_named_lookups($model, @named_lookups); #search each stock to see what variables it contains and #set those as flows $model = &get_flows($model); &remove_old_temp_files(); #intended to remove any tempfiles created more than 30 min ago $model->print_model_contents($parsed_input); &html_start($user_input); #END_DATA_PARSE################################################## sub slider_option_javascript{ #contains the javascript code to hide the slider options qq | |; } sub swap_table_javascript{ qq | |; } sub condense_equations_javascript{ qq| |; } sub css_table_style{ #contains the css code for the table styles qq| |; } sub get_short_input{ #input: the user-entered vensim equations #this fxn takes the first and last vensim objects from the equations #and makes a text file appropriate for condensing on the form (to lessen clutter) my $i, $j, $line, $string; my $object_total = &get_number_of_objects($model); my @data, @first, @last; open(INFILE2, "$input_file"); $i=0; while($line = ){ #read input line by line chomp $line; $data[$i] = $line; $i+=1; } close(INFILE2); open(SHORT, "> $short_input_file"); $i=0; $j=0; while ($i < @data){ if ($data[$i] =~ /^\([0]*1\)/){ while (!($data[$i] =~ /^\s*$/)){ $first[$j] = $data[$i]; $j++; $i++; } } elsif ($data[$i] =~ /^\([0]*$object_total\)/){ $j=0; while (!($data[$i] =~ /^\s*$/)){ $last[$j] = $data[$i]; $j++; $i++; } } else {$i++;} } foreach $line (@first){ $string = $string . $line; } $string = $string . "\t\t.\n\t\t.\n\t\t.\n"; #print vertical elipse (tabbed over to look nice) foreach $line (@last){ $string = $string . $line; } print SHORT $string; close(SHORT); return $string; } sub html_start{ my ($input) = shift; my (%flows) = %{%$model->{'flow'}}; my (%stocks) = %{%$model->{'stock'}}; my (%variables) = %{%$model->{'variable'}}; my (%constants) = %{%$model->{'constant'}}; my ($stock_name); my $slider_init; print header(-type => 'text/html'); print start_html( -title=>"Vensim2Java - Applet Settings", -bgcolor=>"#ffffff") . slider_option_javascript() . swap_table_javascript() . css_table_style(); print h2("
Vensim 2 Java Applet Options Form
"); print table({width=>'80%', style=>'border-style:none', align=>'center'}, Tr(td( "
The Vensim equations are printed below and are condensed for your convenience. Click 'expand' to view the entire equation text or 'collapse' to shorten it. If you made a mistake while submitting, please hit the back button on your browser and try again. Otherwise, scroll down, answer a few questions about your model, and submit the form to create your applet.

" ))); my $shortinput = &get_short_input(); print "
"; print "\n"; print qq|
|; print "[expand]"; print "
"; print "
"; print qq|"; print qq|
\n|; print table({width=>'600', align=>'center', style=>'border-width:1px'}, Tr( td({style=>'background-color:#efefef'}, pre($shortinput)))), br, br; print "
"; #start making the form print start_form(-method=>'post', -action=>'v2j_make_applet.cgi'); print "\n"; #pass unique name to next script print "\n"; #stocks... if ( scalar(keys(%stocks)) != 0 ){ print "\n"; print "\n"; $i = 0; foreach $stock_name (keys %stocks){ print "\n"; if (is_even($i)) { print qq|\n"; } print "
Stocks
\n|; } else { print qq|\n|; } print p, "For stock $stock_name, please answer the following:
", checkbox(-name=>"vargraph$i", -label=>"Graph this variable"), p, checkbox(-name=>"variv$i", -label=>"Use a slider to set the initial value", -onclick=>"hide_slider_options(document.getElementById('varID$i'))"), "
"; print "
\n"; $i++; print "
"; } #flows... if ( scalar(keys(%flows)) != 0 ){ print "
\n"; print "\n"; $i=0; foreach $flow_name (keys %flows){ my $lookup_var, $lookup_datapoints; my @lookup; print "\n"; if (is_even($i)) { print qq|... (rows), # td() = (columns). ############################################################ #remove leading trailing parenths for cosmetic look on form while($lookup_var =~ s/^\(//){ $lookup_var =~ s/\)$//; } print "For flow $flow_name (containing a lookup table), please answer the following:
", p, table({border=>'undef', align=>'center', cellpadding=>'3', style=>'border-width:1px'}, Tr(th( {style=>'border-width:1px;color:#ffffff;'}, [" $lookup_var ", " $flow_name "])), #table header $rows #apply previously created html syntax for table ), p, #get graph type "Graph type:", br, radio_group(-name=>"flowgraphtype$i", -values=>['0', '1'], -default=>'0', -labels=>{'0'=>'Line Graph ', '1'=>'Box Graph '}), p; } else{ print p, "For flow $flow_name, please answer the following:
"; } print checkbox(-name=>"flowgraph$i", -label=>"Allow flow to be graphed"), p, "
"; $i++; print "
\n"; } print "
Flows
\n|; } else { print qq|\n|; } if ($flows{$flow_name}->{'is_lookup'} == 1){ #find the values of the lookup table (the independent variable it # uses, and the datapoints for the table). the string # returned by parse_lookup is "variable:datapoints", so # split it accordingly. $lookup_var = &parse_lookup($flows{$flow_name}->{'value'}); @lookup = split(":",$lookup_var); $lookup_var = $lookup[0]; $lookup_datapoints = $lookup[1]; ########################################################### #build html table to display the values of the lookup table my @xy_values; #each array element contains an x,y value #put x,y values into array: $lookup_datapoints =~ s/^\(//; #remove leading ( $lookup_datapoints =~ s/\)$//; #remove trailing ) @xy_values = split(/\),\(/, $lookup_datapoints); #split between each x,y pair my $rows = ''; foreach my $pair (@xy_values){ $pair =~ m/(.*),(.*)/; #$1=x and $2=y for each x,y pair $rows .= Tr({-align=>'center'},td([$1,$2])); } # $rows is a string containing the valid html syntax for a table # thanks to the CGI.pm module (Tr() =
...
"; } #aux variables... if ( scalar(keys(%variables)) > 1 ){ #condition is >1 instead of >0 because #SAVE_PER is counted as a var print "
\n"; print "\n"; $i=0; foreach $variable_name (keys %variables){ my $lookup_var, $lookup_datapoints, $var_type; my @lookup; if ($variable_name ne "SAVEPER"){ print "\n"; if (is_even($i)) { print qq|... (rows), # td() = (columns). ############################################################ #remove leading trailing parenths for cosmetic look on form while($lookup_var =~ s/^\(//){ $lookup_var =~ s/\)$//; } print p, "For lookup table $variable_name, please answer the following:
", p, table({border=>'undef', align=>'center', cellpadding=>'3', style=>'border-width:1px'}, Tr(th( {style=>'border-width:1px;color:#ffffff;'}, [" $lookup_var ", " $variable_name "])), #table header $rows #apply previously created html syntax for table ), p, #get graph type "Graph type:", br, radio_group(-name=>"auxgraphtype$i", -values=>['0', '1'], -default=>'0', -labels=>{'0'=>'Line Graph ', '1'=>'Box Graph '}), p; } elsif ($variables{$variable_name}->{'is_ite'} == 1) { $var_type = "conditional variable"; print p, "For the conditional variable $variable_name, please answer the following:
"; #the commented-out code is for possible future implementation #allowing the user to change the conditional parameters. =pod my @nested_ites = &parse_ite($variable_name, $variables{$variable_name}->{'value'}); my $tempvalue; $tempvalue = $variables{$variable_name}->{'value'}; $tempvalue =~ s/,/, /g; print checkbox(-name=>"auxcond$i", -label=>"Change conditional parameters", -onclick=>"hide_slider_options(document.getElementById('auxcondID$i'))"), "
"; print "
\n"; =cut } #foreach $myite (@{%{$variables{$variable_name}->{'ites'}}}) { # print "$myite", p; #} else{ $var_type = "variable parameter"; print p, "For variable parameter $variable_name, please answer the following:
"; } print checkbox(-name=>"auxgraph$i", -label=>"Allow $var_type to be graphed"), p checkbox(-name=>"auxiv$i", -label=>"Replace $var_type with a slider", -onclick=>"hide_slider_options(document.getElementById('auxID$i'))"), "
"; print "
"; $i++; print "
\n"; } } print "
Auxillary Variables
\n|; } else { print qq|\n|; } #for lookup tables... if ($variables{$variable_name}->{'is_lookup'} == 1){ $var_type = "lookup"; #find the values of the lookup table (the independent variable it # uses, and the datapoints for the table). the string # returned by parse_lookup is "variable:datapoints", so # split it accordingly. $lookup_var = &parse_lookup($variables{$variable_name}->{'value'}); @lookup = split(":",$lookup_var); $lookup_var = $lookup[0]; $lookup_datapoints = $lookup[1]; ########################################################### #build html table to display the values of the lookup table my @xy_values; #each array element contains an x,y value #put x,y values into array: $lookup_datapoints =~ s/^\(//; #remove leading ( $lookup_datapoints =~ s/\)$//; #remove trailing ) @xy_values = split(/\),\(/, $lookup_datapoints); #split between each x,y pair my $rows = ''; foreach my $pair (@xy_values){ $pair =~ m/(.*),(.*)/; #$1=x and $2=y for each x,y pair $rows .= Tr({-align=>'center'},td([$1,$2])); } # $rows is a string containing the valid html syntax for a table # thanks to the CGI.pm module (Tr() =
...
"; } #constants... NOTE: stell2java counts down for this (counts up here) if ( scalar(keys(%constants)) > 3 ){ #condition is >3 instead of >0 because #FINAL_TIME, TIME_STEP, and INITIAL_TIME #are counted as constants print "
\n"; print "\n"; $i = 0; foreach $constant_name (keys %constants){ if ($constant_name ne "FINAL_TIME" && $constant_name ne "TIME_STEP" && $constant_name ne "INITIAL_TIME"){ print "\n"; if (is_even($i)) { print qq|\n"; } } print "
Constants
\n|; } else { print qq|\n|; } print p, "For constant $constant_name, please answer the following:
", checkbox(-name=>"paramgraph$i", -label=>"Allow constant to be graphed"), p, checkbox(-name=>"paramiv$i", -label=>"Use a slider to set the initial value", -onclick=>"hide_slider_options(document.getElementById('paramID$i'))"), "
"; print "
"; $i++; print "
"; } #graph options print "
\n"; print "
Graph Options
\n"; print p, br, "Please enter the following responses regarding your default independent variable display preferences (taken from Vensim):", p, "Initial time: ", textfield(-name=>'inittime', -default=>"$constants{'INITIAL_TIME'}->{'value'}"), p, "Final time: ", textfield(-name=>'finaltime', -default=>"$constants{'FINAL_TIME'}->{'value'}"), p, "Time step: ", textfield(-name=>'timestep', -default=>"$constants{'TIME_STEP'}->{'value'}"); print p, br, "Please enter the following responses regarding your default dependent variable display preferences (defaults used by vensim2java):",p, "Y-axis minimum: ", textfield(-name=>'ymin', -default=>"0"), p, "Y-axis maximum: ", textfield(-name=>'ymax', -default=>"50"); print p, br, "Please enter a graph title and labels, or leave blank if you do not want one:", p, "Graph title: ", textfield(-name=>'title'), p, "Y-axis label: ", textfield(-name=>'depLabel'); print "
"; print p, "
", reset(-value=>'Reset Form'), "    ", submit(-value=>'Submit Form'), "
"; print endform; } sub html_empty_error{ print header(-type => 'text/html'); print start_html( -title=>"Vensim2Java - Error", -bgcolor=>"#ffffff"); print h2("
Vensim2Java Error Report
"); print table({width=>'60%', align=>'center', cellpadding=>'15', style=>'border:solid;border-collapse:collapse;border-width:2px'}, Tr( td({style=>'background-color:#e5d8b4;'}, "Vensim2Java encountered an error while processing the Vensim equations. The model you have submitted appears to be empty. Note that the equations should be copied directly from Vensim.", br, br, "If you cannot find the cause of this error, please visit the bug page for more help, or to submit a bug.") )); exit; } sub html_loop_error{ #called when v2j_make_form gets stuck parsing each line - this is important #because the only indicator for the end of an object's equation is #the start of the "Units: ...." line. if this line isnt found, for whatever #reason, the script will get stuck in an infinite loop and rup up the server cpu print header(-type => 'text/html'); print start_html( -title=>"Vensim2Java - Error", -bgcolor=>"#ffffff"); print h2("
Vensim2Java Error Report
"); print table({width=>'60%', align=>'center', cellpadding=>'15', style=>'border:solid;border-collapse:collapse;border-width:2px'}, Tr( td({style=>'background-color:#e5d8b4;'}, "Vensim2Java encountered an error while processing the Vensim equations. A valid model could not be created based on the provided input. Note that the equations should be copied directly from Vensim's documentation window.", br, br, "If you cannot find the cause of this error, please visit the bug page for more help, or to submit a bug.") )); exit; } sub html_error{ #if it is detected that a variable contains invalid characters, an #error is thrown. my ($invalid_name) = shift; print header(-type => 'text/html'); print start_html( -title=>"Vensim2Java - Error", -bgcolor=>"#ffffff"); print h2("
Vensim2Java Error Report
"); print table({width=>'60%', align=>'center', cellpadding=>'15', style=>'border:solid;border-collapse:collapse;border-width:2px'}, Tr( td({style=>'background-color:#e5d8b4;'}, "Vensim2Java encountered an error while processing the Vensim equations. The variable:
", br, $invalid_name, br, br, "
contains one or more invalid characters. Although it may be an acceptable variable name in Vensim, Vensim2Java only allows alphanumeric characters (including underscores and whitespace).", br, br, "If you cannot find the cause of this error, please visit the bug page for more help, or to submit a bug.") )); exit; }