1    /*
2        Copyright (C) 2000  Ralph Hartley
3    
4        This program is free software; you can redistribute it and/or modify
5        it under the terms of the GNU General Public License as published by
6        the Free Software Foundation; either version 2 of the License, or
7        (at your option) any later version.
8    
9        This program is distributed in the hope that it will be useful,
10       but WITHOUT ANY WARRANTY; without even the implied warranty of
11       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12       GNU General Public License for more details.
13   
14       You should have received a copy of the GNU General Public License
15       along with this program; if not, write to the Free Software
16       Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17   */
18   
19   import java.awt.event.*;
20   import java.awt.*;
21   import java.awt.geom.*;
22   import java.util.*;
23   import java.io.*;
24   import java.util.zip.*;
25   import java.net.*;
26   import java.util.jar.*;
27   import java.awt.print.*;
28   import java.awt.image.*;
29   import javax.swing.*;
30   import java.lang.ref.*;
31   import java.text.NumberFormat;
32   
33   /**
34    *The main class of the Carto program.
35    *Contains everything that is found in a .cto file.<br>
36    *As a general rule, variables and methods should not be in this file
37    *unless there is no other place they can go.
38    */
39   public class Carto implements Serializable {
40   
41     private static final long serialVersionUID = Version.getSUID();
42   
43     /**
44      *The base from which carto files are relative.
45      *(if any)
46      */
47     static File filebase = new File(System.getProperty("user.dir"));
48   
49     static GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
50     static GraphicsConfiguration gconf = genv.getDefaultScreenDevice().getDefaultConfiguration();
51   
52     public static Selection cutbuffer = null;
53   
54     public transient Selection[] selections = new Selection[4];
55   
56     /**The set of sizes for this file
57      *@deprecated Replaced by defaults.
58      */
59     public TreeSet sizes;     //legacy
60   
61     /**
62      *Default overrides for this file.
63      *Keys are types, values are sets of FileDefaultables of that type. */
64     public HashMap defaults = new HashMap(5,5);
65   
66     /**Survey data for this file. */
67     public Survey survey = null;
68   
69     /**Composites in this file.
70      *This only includes top level Composites. There may be composites included in
71      *the file that are not in complist (usr defined symbols etc).*/
72     public Vector complist = new Vector();
73   
74     /**Segments in this file.
75      *ALL segments referenced from this must be in segmentlist. 
76      *Does not include section and profile segments */
77     public Vector segmentlist = new Vector();
78   
79     /**Cross sections in this file.
80      *ALL sections referenced from this must be in sectionlist. */
81     public Vector sectionlist = new Vector();
82   
83     /**The states of the editors that were open when last saved. */
84     public EditState[] place = null;
85   
86     /**
87      *PrefEditors for this file.
88      *These contain the file specific preferences. */
89     public PrefEditor[] prefs = null;
90   
91     /**The default screen unit.
92      *New files get their screen unit from here.
93      *Is set by a global Pref. */
94     public static Unit defscreenunit = Unit.inch;
95   
96     /**The default map unit.
97      *New files get their map unit from here.
98      *Is set by a global Pref. */
99     public static Unit defmapunit = Unit.foot;
100  
101    /**The screen unit.
102     *This is the default unit for distances on the screen.
103     *It is not really used for much.*/
104    public Unit screenunit = defscreenunit;
105  
106    /**The map unit.
107     *Determines what units the survey data are defined in terms off.
108     *If not set correctly, survey data will not be interpreted correctly.
109     */
110    public Unit mapunit = defmapunit;
111  
112    /**Represents the version running now. */
113    public static Version runversion = new Version();
114  
115    /**The version of Carto that created this file. */
116    public String created_version = runversion.getVersion();
117    /**The version of Carto that last saved this file. */
118    public String saved_version = runversion.getVersion();
119  
120    /** The directory containing the .cto file<br>
121     *Used for finding images if things have moved.
122     */
123    public transient File base = null;
124    /** If the file was int a zip, the entry it was in.
125     */
126    public transient ZipEntry zipent = null;
127    /**
128     *If the file was in a zip the zip file it was in.
129     */
130    public transient ZipFile zipfile = null;
131  
132    /**The thread in which the background job runs.
133     *If null, then there is no background job.
134     *Background jobs are used for cooking and printing. Only
135     *one is allowed at a time because they are not thread safe.
136     *Background jobs must clear this pointer when the end.
137     */
138    public static Thread background = null;
139    /**A flag that tells the background job that it should die. 
140     *Background jobs should clear this flag when they die.*/
141    public static int kill = 0;
142  
143    /**Indicates that a cook job should die. */
144    public static int COOKKILL = 1;
145    /**Indicates that a print job should die. */
146    public static int PRINTKILL = 1;
147  
148    /**Create a new, empty carto file. */
149    public Carto() {
150      for (int i=0;i<4;i++)
151        selections[i] = new Selection(this);
152  
153      FileDefaultable.sortOut(defaults);
154    }
155  
156    /**
157     *Prompt the user for a .cto file */
158    public static File choose(Component parent) {
159      JFileChooser choose = Persist.current.getFileChooser();
160  
161      choose.setFileFilter(new FileTypes(FileTypes.CARTO));
162      choose.rescanCurrentDirectory();
163      if (choose.showOpenDialog(parent)!=choose.APPROVE_OPTION) return(null);
164      return(choose.getSelectedFile());
165    }
166  
167    /**
168     *Read a Carto from a file.
169     *Sorts out its defaults.
170     *@returns null if the file has a problem
171     */
172    public static Carto read(File filename) {
173      Object result = null;
174      File base = null;
175      ZipFile zip = null;
176      ZipEntry zipent = null;
177      try {
178        InputStream istream = null;
179        if (filename.getName().substring(filename.getName().lastIndexOf('.')+1).equals("cto")) {
180          istream = new FileInputStream(filename);
181          base = filename.getCanonicalFile().getParentFile();
182        }
183        else {
184          base = filename.getCanonicalFile();
185          zip = new ZipFile(filename);
186  
187          Vector ctos = new Vector();
188          for (Enumeration zipnames = zip.entries();
189               zipnames.hasMoreElements();) {
190            ZipEntry ent = (ZipEntry)zipnames.nextElement();
191            String name = ent.getName();
192            int dot = name.lastIndexOf('.');
193            if (dot>=0 && name.substring(dot).equals("cto"))
194              ctos.add(ent);
195          }
196          if (ctos.size()==0) throw new FileNotFoundException(filename.getName()+
197          						     " does not conatain any .cto files");
198          else if (ctos.size()==1) zipent = (ZipEntry)ctos.elementAt(0);
199          else {
200            Object[] names = ctos.toArray();
201            zipent = (ZipEntry)JOptionPane.showInputDialog(null,
202          					      "Choose .cto file from zip",
203          					      "Zip",
204          					      JOptionPane.QUESTION_MESSAGE,
205          					      null,
206          					      names,
207          					      names[0]);
208          }
209          if (zipent==null) return(null);
210          istream = zip.getInputStream(zipent);
211        }
212        ObjectInputStream in = new ObjectInputStream(istream);
213        result = in.readObject();
214        istream.close();
215      } catch (Exception e) {
216          ErrorLog.log("Carto file not sucessfully read: "+filename);
217          ErrorLog.exception(e);
218          return(null);
219      }
220  
221      if (result == null || !(result instanceof Carto)) {
222        ErrorLog.log("error: could not read Carto data from "+filename+".");
223        return(null);
224      }
225      else {
226        Carto res = (Carto)result;
227        FileDefaultable.sortOut(res.defaults);
228        res.zipfile = zip;
229        res.base = base;
230        res.zipent = zipent;
231        return(res);
232      }
233    }
234  
235    /**
236     *Get ready for Survey update.
237     */
238    public void prepareForSurveyUpdate() {
239      for (Iterator it = complist.iterator();it.hasNext();) {
240        ((Comp)it.next()).prepareForSurveyUpdate(null);
241      }
242    }
243  
244    /**
245     *Use a survey update.
246     *Move symbols to follow changed morph.
247     */
248    public void useSurveyUpdate() {
249      for (Iterator it = complist.iterator();it.hasNext();) {
250        ((Comp)it.next()).useSurveyUpdate();
251      }
252    }
253  
254    /**
255     * Gets list of eliments by type 
256     */
257    public Vector getList(String name) {
258      if (name.equals(CartoFrame.SEGWORD)) return(segmentlist);
259      else if (name.equals(CartoFrame.COMPWORD)) return(complist);
260      else if (name.equals(CartoFrame.SECTION)) return(sectionlist);
261      return(new Vector()); //Should never happen.
262    }
263  
264    /**
265     *Cleans up trash.
266     *Deletes Segments that are referenced but not listed.
267     *Put code to fix other inconsitent states here. */
268    public void clean() {
269      for (Iterator it = complist.iterator();it.hasNext();)
270        ((Comp)it.next()).clean(segmentlist);
271    }
272  
273    /**
274     *Tells if station is used anywhere,<br>
275     *other than in survey.
276     *If so, returns string saying how.
277     *Otherwise returns null.
278     *Used to decide if safe to delete station from survey.
279     */
280    public String used(Vertex station) {
281      String res = "";
282  
283  //    for (Iterator it = complist.iterator();it.hasNext();) {
284  //      String str = ((Comp)it.next()).used(station);
285  //      if (str!=null) res += "\n"+str;
286  //    }
287  
288      for (Iterator it = segmentlist.iterator();it.hasNext();) {
289        Segment seg = (Segment)it.next();
290        if (seg.map.used(station))
291          res += "   to morph segment "+seg+"\n";
292      }
293  
294      if (res.length()==0) return(null);
295      return(res);
296    }
297  
298    /**
299     *Remove any use of station.<br>
300     *Used to make it safe to delete station from survey.
301     */
302    public void remove(Vertex station) {
303  //    for (Iterator it = complist.iterator();it.hasNext();)
304  //      ((Comp)it.next()).remove(station);
305  
306      for (Iterator it = segmentlist.iterator();it.hasNext();)
307        ((Segment)it.next()).map.remove(station);
308    }
309  
310  
311    /**Start a background job.
312     *Makes sure that there is not one already. */
313    public static boolean startBackground(Thread action) {
314      synchronized(Carto.class) {
315        if (background != null) {
316          ErrorLog.log("There is already a background process going on\n"+
317          	     "It should be stoped before starting another\n");
318          return(false);
319        }
320        background = action;
321        background.setPriority(background.getPriority()-1);
322        background.start();
323        return(true);
324      }
325    }
326  
327    /**Indicate that background has died.
328     *Must only be called by the background job.
329     *Must always be called by any background job before dieing */
330    public static void cleanBackground() {
331      synchronized(Carto.class) {
332        background = null;
333        kill = 0;
334      }
335    }
336  
337    /**Kill the background job.
338     *Waits for the job to die. */
339    public static void stopBackground(int killval) {
340      Thread deadthread;
341      synchronized(Carto.class) {
342        deadthread = Carto.background;
343        if (deadthread!=null)
344            Carto.kill = killval;
345      }
346      if (deadthread!= null)
347        while (background==deadthread) {
348          try {
349            Thread.sleep(50);
350          } catch (InterruptedException e) {}
351        }
352    }
353  
354    /**
355     *Recook all segments.<br>
356     *Throws out the morphed version of all segments
357     *and forces them to be remorphed from scratch. */
358    public void reCookAll(final ThreadMessage message,final CartoFrame owner) {
359  //    ErrorLog.log("recookall debug");
360  
361      if (Segment.cookondemand) {
362        ImageThread.stopLoading();
363        for (Iterator it = segmentlist.iterator();it.hasNext();) {
364          Segment seg = (Segment) it.next();
365          seg.doCook(null);
366        }
367      }
368      else {
369  
370        startBackground(new Thread(){
371            public void run() {
372              try {
373                for (Iterator it = segmentlist.iterator();it.hasNext();) {
374          	Segment seg = (Segment) it.next();
375          	seg.unCook();
376          	if (seg.cookable()) seg.cook(null);
377                }
378              } catch (CookKillException killex) {
379              } finally {
380                owner.repaint();
381                cleanBackground();
382              }
383            }}); 
384      }
385    }
386  
387    /**
388     *Hard recook of all segments.<br>
389     *Currently this just calls reCookAll, because there is no
390     *soft recook anymore.
391     */
392    public void hardReCookAll(final ThreadMessage message,final CartoFrame owner) {
393      reCookAll(message,owner);
394    }
395  
396    /**
397     *Get the defaults of a given type.
398     *Returns the set of default overides, if any. */
399    public TreeSet getDefs(Class type) {
400      if (!defaults.containsKey(type))
401        defaults.put(type, new TreeSet());
402      return((TreeSet)defaults.get(type));
403    }
404  
405    /**Reads a Carto from a stream.
406     *We define this method to fix up the old file to work with the current version. */
407    private void readObject(java.io.ObjectInputStream stream)
408        throws java.io.IOException,java.lang.ClassNotFoundException {
409      stream.defaultReadObject();
410  
411      //fix legacies from before PrefDef existed
412  
413      if (defaults==null) defaults = new HashMap(5,5);
414  
415      if (!defaults.containsKey(Size.class)) {
416        if (sizes!=null) defaults.put(Size.class,sizes);
417        else defaults.put(Size.class,new TreeSet());
418      }
419  
420      if (mapunit==null) mapunit = defmapunit;
421      if (screenunit==null) screenunit = defscreenunit;
422  
423      if (sectionlist==null) sectionlist = new Vector();
424  
425      if (selections==null || selections[0]==null) {
426        selections = new Selection[4];
427        for (int i=0;i<4;i++)
428          selections[i] = new Selection(this);
429      }
430    }
431  
432    /**Gets the names of all the contents of a directory in a Jar file */
433    public static String[] getSubdirList(JarFile source,String dirname) throws IOException{
434      Vector list = new Vector();
435      for (Enumeration entries  = source.entries();
436           entries.hasMoreElements();) {
437        String name = ((JarEntry)entries.nextElement()).getName();
438        if (name.startsWith(dirname)
439            && !name.equals(dirname)) list.add(name);
440      }
441      String[] res = new String[list.size()];
442      for (int i=0;i<list.size();i++) res[i] = (String)list.elementAt(i);
443      return(res);
444    }
445  
446    /**Copies the named files from the Jar file.
447     *Puts the results in the base directory. If any of
448     *the named files are directories copies the files they contain
449     *as well */
450    public static void copyFiles(JarFile source,String[] names) throws IOException{
451      for (Enumeration entries  = source.entries();
452           entries.hasMoreElements();) {
453        JarEntry ent = (JarEntry)entries.nextElement();
454        String name = ent.getName();
455        for (int i=0;i<names.length;i++) {
456          if (name.equals(names[i])) {
457            if (ent.isDirectory()) {
458              (new File(filebase,name)).mkdir();
459  //            System.out.println("base = "+base+" dir = "+name);
460              String[] subs = getSubdirList(source,name);
461  //            System.out.println("subs = "+subs);
462              copyFiles(source,subs);
463            } else {
464              File dest = new File(filebase,name);
465              try {
466                InputStream instr = source.getInputStream(ent);
467                OutputStream outstr = new FileOutputStream(dest);
468                byte[] buff = new byte[1000];
469                for (int num = 0;num>=0;) {
470          	outstr.write(buff,0,num);
471          	num = instr.read(buff);
472                }
473                outstr.close();
474              } catch (IOException e) {
475                ErrorLog.exception(e,"Cannot write "+dest);
476              }
477            }
478          }
479        }
480      }
481    }
482  
483    /**Copy a file from the jar file to the base directory. */
484    public static void copyFile(String name){
485      URL source = runversion.getClass().getResource(name);
486  //    System.out.println(source);
487      File dest = new File(filebase,name);
488      try {
489        InputStream instr = source.openStream();
490        OutputStream outstr = new FileOutputStream(dest);
491        byte[] buff = new byte[1000];
492        for (int num = 0;num>=0;) {
493          outstr.write(buff,0,num);
494          num = instr.read(buff);
495        }
496        outstr.close();
497      } catch (IOException e) {
498        ErrorLog.exception(e,"Cannot write "+dest);
499      }
500    }
501  
502    public static String[] installfiles = {"gpl.txt","contributors.txt","version.txt","symbol/","stamp/","help/"};
503  //,"example/"};
504  
505    /**Make sure the instalation is complete.
506     *If not, complete it. Uses version.txt to determine
507     *what needs to be unpacked. If needed unpacks
508     *symbol libraries and
509     *optional files (gpl.txt + example files (if any)).
510     *If persist.sta does not exist, create it.*/
511    public static void checkInstall() {
512      File name = new File(filebase,"version.txt");
513      if (!name.exists()) {
514        ErrorLog.logOK("Installing files");
515        try {
516        JarFile source = ((JarURLConnection)(runversion.getClass().
517          		getResource("version.txt").openConnection())).getJarFile();
518        copyFiles(source,installfiles);
519  //      copyDir(source,"symbol",base);
520  //      copyDir(source,"stamp",base);
521  //      copyDir(source,"fill",base);
522        } catch (IOException e) {ErrorLog.exception(e,"trouble installing");}
523      }
524      if (!Persist.statefile.exists()) {
525        ErrorLog.logOK("Creating new default Preferences :"+Persist.statefile);
526        (new Persist()).saveState();
527      }
528    }
529  
530    /** Print some basic statistics for the file. <br>
531     */
532    public void printStats(PrintStream out) {
533      double area = 0;
534      double afact = 0;
535      String aname = "hectares";
536      NumberFormat form = NumberFormat.getNumberInstance();
537      form.setMaximumFractionDigits(2);
538  
539      afact = Unit.meter.toUnit(mapunit); 
540      afact *= afact;
541      afact /= 10000;
542  
543      if (mapunit == Unit.foot) {
544        afact/= 0.4047;
545        aname = "acres";
546      }
547  
548      out.println("last survey update "+survey.name);
549      out.println(""+survey.stations.size()+" stations "+survey.shots.size()+" shots");
550          
551      out.println("\n"+segmentlist.size()+" Segments");
552  
553      int namemax = 0;
554      int filemax = 0;
555      for (Iterator it = segmentlist.iterator(); it.hasNext();) {
556        Segment seg = (Segment)it.next();
557        int length = seg.name.length(); 
558        if (length>namemax) namemax = length;
559        length = seg.isource.source.getName().length();
560        if (length>filemax) filemax = length;
561      }
562  
563      for (Iterator it = segmentlist.iterator(); it.hasNext();) {
564        Segment seg = (Segment)it.next();
565        double sarea = seg.area();
566        area += sarea;
567        StringBuffer buff = new StringBuffer();
568        buff.append(seg.name);
569        while (buff.length() < namemax+5) buff.append(' ');
570        buff.append("file = "+seg.isource.source.getName());
571        while (buff.length() < namemax+filemax+18) buff.append(' ');
572        buff.append("area = "+form.format(sarea)+" sq "+mapunit+
573          	  " ("+form.format(sarea*afact)+" "+aname+")");
574        out.println(buff);
575      }
576      out.println("\nTotal area of segments = "+form.format(area)+" square "+mapunit+
577          	" ("+form.format(area*afact)+" "+aname+")");
578  
579      area = Segment.totalArea(segmentlist);
580      out.println("\nEstimated Cave area = "+form.format(area)+" square "+mapunit+
581          	" ("+form.format(area*afact)+" "+aname+")");
582  
583      out.println("\n"+complist.size()+" Composites");
584  
585      for (Iterator it = complist.iterator(); it.hasNext();)
586        out.println(""+((Comp)it.next()).name);
587    }
588  
589    /**Start the Carto program.
590     *Initializes error log, reads persistant state,
591     *prints about message, and shows a new CartoFrame.<br>
592     *CartoFrame does most top level functions, but it has the wrong name
593     *to be the main class. */
594    static public void main(String args[]) {
595      boolean stats = false;
596      int i;
597  
598      String filename = null;
599      for (i=0;i<args.length;i++) {
600        if (args[i].equals("-b")) {
601          filebase = new File(filebase,args[i+1]);
602          i++;
603        }
604        if (args[i].equals("-s")) {
605  //        filename = args[i+1];
606          stats = true;
607          i++;
608          break;
609        }
610        else if (i==args.length-1)
611          filename = args[i];
612      }
613  
614      ErrorLog log = null;
615  
616      try {
617        System.setErr(log=new ErrorLog());
618      } catch (Exception e2) {
619        System.out.println("Trying to set up the error file itself caused an error");
620        System.out.println("RunID = "+Persist.current.getRunID());
621        e2.printStackTrace();
622      }
623  
624      checkInstall();
625  
626      Persist.restore();
627  
628      log.logMessage();
629      
630      System.out.print(runversion.getAbout());
631  
632      File startfile = null;
633      if (filename!=null) startfile = new File(filebase,filename);
634  
635      if (stats) {
636        try {
637  //        if (i==args.length) {
638  //          System.out.println("\nCarto File "+startfile.getName());
639  //          read(startfile).printStats(System.out);
640  //        }
641          for (;i<args.length;i++) {
642            File file = new File(filebase,args[i]);
643            System.out.println("\nCarto File "+file.getName());
644            read(file).printStats(System.out);
645          }    
646        } catch (Exception ex) {
647          log.exception(ex,"Trouble printing stats");
648          System.exit(1);
649        }
650      }
651      else {
652        CartoFrame frame = new CartoFrame(startfile);
653  
654        frame.show();
655        
656        frame.setLocation(CartoFrame.initialx,CartoFrame.initialy);
657      }
658    }        			 
659  }
660  
661