1    /*
2    Copyright 2002 by Ralph Hartley
3    This software is licenced under the terms of the
4    Gnu Public Licence
5    */
6    import java.io.*;
7    import java.awt.*;
8    import java.awt.image.*;
9    import java.awt.geom.*;
10   import java.util.*;
11   import javax.swing.*;
12   import java.awt.event.*;
13   
14   public class Segment extends Symbol implements Editable,ActionListener{
15   
16     private static final long serialVersionUID = Version.getSUID();
17   
18     static public Color backc = Color.white;
19     static public Color borderc;
20     static public Color stationc;
21     static public Color surveyc;
22     static public Color selectc;
23     static public boolean removeunmorphed = false;
24       //  static public double defdensity = 1.0;
25     static public double defscale = 1;
26     static public boolean deftransparent = false;
27    
28     static public String MORPH = "Remorph";
29     static public String EDIT = "Edit";
30   
31     File source = null;
32     ImageFile isource = null;
33     Survey set;
34     public SegBoundary boundary;
35     Mapping map;
36     int fixcnt = 0;
37     boolean dirty = false;
38   
39     static boolean openscaled = true;
40   
41   //  public double density = defdensity;
42     public boolean transparent = deftransparent;
43   
44     public double cookfact = 1.0;
45   
46     public static int MORPHABLE = 1;
47     public static int SOLVED = 2;
48     public static int MORPHED = 4;
49     public static int LINEAR = 8;
50     public static int WORKING = 16;
51     public static int READING = 32;
52     public static int QUEUED = 32;
53   
54     public int status = 0;
55     transient Image hardcooked = null;
56   
57     public transient BufferedImage thumbnail = null;
58     
59     AffineTransform cooktransform = null;
60   
61     public static boolean cookondemand = true;
62     public static boolean thumbs = true;
63     public static boolean lockall = true;
64     public static int levels = 5;
65     public static boolean dobig = false;
66     public static boolean usebigger = true;
67     public static boolean usesmaller = true;
68   
69     public ImageJob[] cookers;
70     public transient double oldscale = 0;
71     public transient int lastindex = -1;
72     public double overlayscale = 1;
73   
74     public int thumbw = 0;
75     public int thumbh = 0;
76     public Object thumbdata = null;
77   
78     public Segment() {super();}
79   
80     private void readObject(java.io.ObjectInputStream stream)
81         throws java.io.IOException,java.lang.ClassNotFoundException {
82       stream.defaultReadObject();
83       if (source!=null && isource==null) isource = ImageFile.getImageFile(source,null);
84       source = null;
85   
86       if (thumbw!=0 && thumbs) {
87         thumbnail = new BufferedImage(thumbw,thumbh,BufferedImage.TYPE_INT_ARGB);
88         thumbnail.getRaster().setDataElements(0,0,thumbw,thumbh,thumbdata);
89       }
90       else thumbnail = null;
91   
92       if (thumbs && cookers!=null && cookers[levels-1]!=null)
93           cookers[levels-1].setImage(thumbnail);
94   
95   //    if (density==0) density = defdensity;
96   //    density = 1;
97   //    transparent = false;
98     }
99   
100    private void writeObject(java.io.ObjectOutputStream stream)
101        throws java.io.IOException,java.lang.ClassNotFoundException {
102  
103      if (thumbnail==null || !thumbs) {
104        thumbw = 0;
105        thumbdata = null;
106      }
107      else {
108        Raster rast = thumbnail.getRaster();
109        thumbw = rast.getWidth();
110        thumbh = rast.getHeight();
111  //      System.out.println("seg = "+name+" size = ("+thumbw+","+thumbh+")");
112  //      try {
113          thumbdata = rast.getDataElements(0,0,thumbw,thumbh,null);
114  //      } catch (RuntimeException ex) {System.out.println("exception "+ex);}
115      }
116  
117      stream.defaultWriteObject();
118    }
119  
120    public synchronized void solve() {
121      if (!map.solved) {
122        map.solve();
123        boundary.map(map);
124      }
125      else if (boundary.mapped == null || boundary.mapped.length!= boundary.npoints)
126        boundary.map(map);
127    }
128  
129    public boolean checkSurvey() {
130      if (set==null/*&&map.isVertical==false*/) {
131        ErrorLog.log("You cannot have a segment without a survey\nopen a survey first");
132        return(false);
133      }
134      return(true);
135    }
136  
137    public Segment(Survey survey,boolean isVertical) {
138      try {
139        set = survey;
140        if (!makenames) name = JOptionPane.showInputDialog("Name of new Segment");
141        checkName();
142        isource = ImageFile.getImageFile(null,null);
143        if (!isource.check(null,null)) {
144          set=null;
145  //        ErrorLog.exception("No image source - invalid segment");
146          return;
147        }
148        else {
149          map = new Mapping(15,isVertical);
150          if (isVertical) map.setScale(defscale);
151          boundary = new SegBoundary();
152        }
153        checkSurvey();
154      } catch (OutOfMemoryError e) {
155        set = null;          //mark as invalid
156        ErrorLog.exception(e,"Out of memory opening segment "+name);
157      }
158    }
159  
160    public Symbol importSym(Carto dest,Carto source) {
161      if (dest.survey==null) dest.survey = source.survey;
162      set = dest.survey;
163      map.reattachStations(set);
164      dirty = true;
165      dest.segmentlist.add(this);
166      return(this);
167    }
168  
169    public boolean check() {return(set!=null);}
170  
171    public Editor getEditor(CartoFrame frame) {
172      try {
173        return(new SegEditor(this,frame));
174      } catch (OutOfMemoryError e) {
175        ErrorLog.exception(e,"Out of memory editing segment "+name);
176        return(null);
177      }
178    }
179  
180    public boolean selectProbe(Point2D where,View view){
181  //    Point2D pos = new Point2D.Double();
182  //    Point2D.Double res = new Point2D.Double();
183      solve();
184      for (int i = 0; i<boundary.npoints;i++) {
185  //      pos.setLocation((double)boundary.xpoints[i],(double)boundary.ypoints[i]);
186  //      map.ftrans(boundary.mapped[i],res);
187        if (boundary.mapped[i].distance(where)<Size.handle.getSize(view.mapscale)/view.scale) return(true);
188      }
189      return(false);
190    }
191  
192    public void showSelected(View view) {
193      Stroke oldstroke = view.draw.getStroke();
194      Stroke newstroke;
195  
196      if (oldstroke instanceof BasicStroke)
197        newstroke = new BasicStroke(((BasicStroke)oldstroke).getLineWidth()*2);
198      else
199        newstroke = new BasicStroke(2);
200  
201      view.draw.setStroke(newstroke);
202  
203      showOutline(view);
204  
205      view.draw.setStroke(oldstroke);
206    }
207  
208    public void showOutline(View view) {
209      AffineTransform trans = (AffineTransform)view.trans.clone();
210      solve();
211      if (map.solved) {
212        Polygon viewborder = new Polygon();
213  //      Point2D.Double pos = new Point2D.Double();
214        Point2D.Double res = new Point2D.Double();
215        for (int i = 0; i<boundary.npoints;i++) {
216  //        pos.setLocation((double)boundary.xpoints[i],(double)boundary.ypoints[i]);
217  //        trans.transform(map.ftrans(pos,res),pos);
218          trans.transform(boundary.mapped[i],res);
219          viewborder.addPoint((int)res.getX(),(int)res.getY());
220        }
221        view.draw.drawPolygon(viewborder);
222      }
223    }
224  
225    public double area() {
226      double area = 0;
227      solve();
228      if (map.solved) {
229        Point2D.Double last = null;
230        last = boundary.mapped[boundary.npoints-1];
231        for (int i = 0; i<boundary.npoints;i++) {
232          area += (boundary.mapped[i].getX() - last.getX()) * (boundary.mapped[i].getY() + last.getY());
233          last = boundary.mapped[i];
234        }
235      }
236  
237      if (area<0) area = -area;
238      return(area/2);
239    }
240  
241    public Polygon getPoly(double scale) {
242      solve();
243  
244      if (map.solved) {
245  
246        int[] xt = new int[boundary.npoints];
247        int[] yt = new int[boundary.npoints];
248  
249        for (int i = 0; i<boundary.npoints;i++) {
250          xt[i] = (int)(scale*boundary.mapped[i].getX());
251          yt[i] = (int)(scale*boundary.mapped[i].getY());
252        }
253        return(new Polygon(xt,yt,boundary.npoints));
254      }
255      return(null);
256    }
257  
258    public static double AFACT = 100.0;
259  
260    public double area(Collection exclude) {
261      if (exclude.size()==0) return(area());
262  
263      Polygon mypoly = getPoly(AFACT);
264      if (mypoly==null) return(0.0);
265      Area me = new Area(mypoly);
266  
267      for (Iterator it=exclude.iterator();it.hasNext();) {
268        Polygon otherpoly = ((Segment)it.next()).getPoly(AFACT);
269        if (otherpoly!=null)
270          me.subtract(new Area(otherpoly));
271      }
272  
273      double lastx=0,lasty=0,firstx=0,firsty=0;
274      double[] coords= new double[6];
275      double area = 0;
276  
277      for (PathIterator path = me.getPathIterator(null);!path.isDone();path.next()) {
278        int seg = path.currentSegment(coords);
279        switch (seg) {
280        case PathIterator.SEG_MOVETO:
281          lastx = firstx = coords[0];
282          lasty = firsty = coords[1];
283          break;
284        case PathIterator.SEG_LINETO:
285          area += (coords[0] - lastx) * (coords[1] + lasty);
286          lastx = coords[0];
287          lasty = coords[1];
288          break;
289        case PathIterator.SEG_CLOSE:
290          area += (firstx - lastx) * (firsty +lasty);
291          break;
292        default:
293          ErrorLog.log("Trouble computing area of segment "+name);
294        }
295      }
296  
297      if (area<0) area = -area;
298      return(area/(2*AFACT*AFACT));
299    }
300  
301    public static double totalArea(Vector all) {
302      double area = 0;
303  
304      for (int i = 0;i<all.size();i++) {
305        Segment cur = (Segment)all.get(i);
306        Vector exclude = new Vector();
307        for (int j=i+1;j<all.size();j++) {
308          Segment other = (Segment)all.get(j);
309          if (cur.excludeOverlap(other))
310            exclude.add(other);
311        }
312        area += cur.area(exclude);
313      }
314      
315      return(area);
316    }
317  
318    public boolean excludeOverlap(Segment other) {
319      for (int i=0;i<map.stacnt;i++)
320        if (other.map.used(map.spoint[i])) return(true);
321      return(false);
322    }
323  
324    public boolean cookable() {
325      if (!cookondemand) {
326        if (map.overlaytrans==null) map.setOverlayTrans();
327        return(boundary.npoints>2 && ((map.stacnt>=2 && map.overlaytrans!= null) || map.isVertical));
328      }
329  
330  //    if (!map.solved) unCook();
331  
332      if (map.overlaytrans==null) map.setOverlayTrans();
333      if (map.overlaytrans==null || map.tchanged) {
334        unCook();
335        map.tchanged = false;
336        cookers = null;
337      }
338  
339      status = (status & ~MORPHABLE) |
340        (boundary.npoints>2 && map.stacnt>=2 && map.overlaytrans!= null ? MORPHABLE :0);
341  
342  
343  //    if (thumbnail!=null && !thumbnail.check()) thumbnail = null;
344      if (cookers!=null && cookers.length!=levels) cookers = null;
345      if (cookers!=null)
346        for (int i=0;i<levels;i++)
347          if (cookers[i]==null || !cookers[i].check()) {
348            cookers = null;
349            thumbnail = null;
350            break;
351          }
352      if (cookers==null && 0!=(status&MORPHABLE)) {
353        overlayscale = Math.sqrt(
354          map.overlaytrans.getScaleX()*map.overlaytrans.getScaleX() +
355          map.overlaytrans.getShearX()*map.overlaytrans.getShearX());
356        cookers = new ImageJob[levels];
357        for (int i=0;i<levels;i++)
358          cookers[i] = new ImageJob(this,i);
359        if (thumbs);
360          cookers[levels-1].setImage(thumbnail);
361      }
362  
363      return(0!=(status&MORPHABLE));
364    }
365  
366    public String getMorphStatusString() {
367      if (status==0) return("Unmorphable ");
368      StringBuffer res = new StringBuffer();
369      if (0!=(status&WORKING))
370        res.append("Morphing ");
371      if (0!=(status&READING))
372        res.append("Reading ");
373      if (0!=(status&QUEUED))
374        res.append("Queued ");
375      if (0!=(status&MORPHED))
376        res.append("Morphed ");
377      if (0!=(status&LINEAR))
378        res.append("Linear ");
379      return(res.toString());
380    }
381  
382    Image getCooked() {
383  //    if (iscooked==false) return(null);
384      if (map.solved) return(hardcooked);
385      else return(null);
386    }
387  
388    void setCooked(BufferedImage cooked) throws CookKillException {
389      hardcooked = cooked;
390    }
391    
392    public ImageJob getCooked(double scale,Component user,boolean wait) {
393  
394      // If the segment is not ready to morph, return failure.
395      if (!cookable()) {
396  //      unCook();
397        return(null);
398      }
399      // Compute the scale at which the morphed image will be displayed.
400      scale /= overlayscale;
401  
402      int cookindex = 0;
403  
404      if (scale==oldscale)
405        cookindex = lastindex;
406      else {
407        // Find the index for the size that is right for the scale.
408        // Too big - Slow, uses lots of memory.
409        // Too small - doesn't show enough detail (blocky).
410        for (cookindex = 0; cookindex<levels-1 && cookers[cookindex].scale*scale*ImageJob.SCALEFACT<1.0; cookindex++);
411      }
412  
413      oldscale = scale;
414      lastindex = cookindex;
415      CacheRef.uses(user,cookers[cookindex]);
416  
417      // If the exact image ordered is available, use that.
418      if (cookers[cookindex].lock(true)) {
419        return(cookers[cookindex]);
420      }
421  
422      int index = 0;
423  
424      // Look for a Bigger (more detailed) version.
425      if (usebigger) for (index=cookindex; index>=0 && !cookers[index].lock(true) ;index--);
426      
427      //If we have a bigger version, get the smaller one, but no need to repaaint.
428      if (index>=0)
429        cookers[cookindex] = ImageThread.startLoad(cookers[cookindex],null);
430  
431      else {
432        if (wait) {
433          //If we don't have a sufficiently detailed image ready.
434          // and we have been told to wait for it, cook it now.
435          return(cookers[cookindex]=ImageThread.loadNow(cookers[cookindex]));
436        }
437        // Display will be updated when the new one is ready.
438        cookers[cookindex] = ImageThread.startLoad(cookers[cookindex],user);
439        // Look for a smaller version to use temporarily.
440        if (usesmaller) 
441          for (index=cookindex+1; index<levels && !cookers[index].lock(true) ;index++);
442        else
443          index = levels;
444      }
445      
446      //If we have nothing at all, report failure.
447      if (index==levels) return(null);
448  
449      return(cookers[index]);
450    }
451  
452    void unCook() {
453      if (cookondemand) {
454        if (cookers!=null)
455          for (int i=0;i<cookers.length;i++)
456            cookers[i] = null;
457        thumbnail = null;
458      }
459      else
460        hardcooked=null;
461    }
462  
463    public Image getImage() {
464      return(isource.getImage());
465    }
466  
467    public void getMorphStatusString(javax.swing.JLabel res) {
468      if (hardcooked==null) res.setText("Unmorphed");
469      else {
470        if (map.degenerate) res.setText("Morphed linear");
471        else res.setText("Morphed");
472      }
473    }
474  
475    public boolean cook(SegEditor editor) throws CookKillException{
476      if (map.overlaytrans==null) map.setOverlayTrans();
477      if (!cookable()) {
478        ErrorLog.log("Carto can only morph a segment that has a boudary\n"+
479          	   "and has at least 3 stations located (or is a cross section)");
480        unCook();
481        if (editor!=null) getMorphStatusString(editor.status);
482        return(false);
483      }
484      Image rawimage = null;
485      try {
486        if (editor!=null) editor.status.setText("Morphing in progress");
487  //      cooking = true;
488  //      message.setText(name);
489        unCook();
490  
491        rawimage = getImage();
492        if (rawimage==null)
493          return(false);
494  
495        solve();
496        cooktransform = map.overlaytrans;
497        java.awt.geom.Rectangle2D bounds = getBounds(cooktransform);
498        setCooked(map.getImage(rawimage,cooktransform,bounds,boundary,
499          		     name,transparent));
500        cooktransform = map.overlaytrans.createInverse();
501        cooktransform.translate(bounds.getX(),bounds.getY());
502  
503      } catch (OutOfMemoryError e) {
504        ErrorLog.exception(e,"Out of memory morphing segment "+name);
505        unCook();
506      } catch (Exception e) {
507        if (e instanceof CookKillException) throw (CookKillException) e;
508        ErrorLog.exception(e,"Error morphing segment "+name);
509      } finally { 
510        CartoFrame.message.setText("");
511        rawimage = null;
512        if (editor!=null) getMorphStatusString(editor.status);
513      }
514      return(true);
515    }
516  
517    transient boolean exdone = false;
518  
519    public void ptest(String pt) {
520      if (0==name.compareTo("sc11")) {
521        System.out.print(name);
522        if (cookers!=null) {
523          System.out.println(" "+pt+" "+cookers[4]);
524          if (cookers[4]!=null) System.out.println(cookers[4].isReady());
525          else System.out.println("*** cookers = null ***");
526        }
527      }
528    }
529  
530    public void paint(View view) {
531      try {
532        if (!view.visible.isMember(this)) return;
533        AffineTransform trans = (AffineTransform)view.trans.clone();
534        Rectangle2D bound = getBounds(trans);
535        if (bound!= null && view.draw.getClip().intersects(bound)) {
536  //      if (true) {
537          if (cookondemand) {
538            ImageJob cooked = getCooked(view.scale,view.parent,view.wait);
539            if (cooked!=null) {
540              trans.concatenate(cooked.trans);
541              view.draw.drawImage((Image)cooked.get(),trans,view.observer);
542              if (!lockall) cooked.lock(false);
543            }
544            else if (cookable()) {
545              showOutline(view);
546            }
547            else view.draw.drawString(""+name+" not cooked",30,30);
548          }
549          else {
550            
551            Image cooked = getCooked();
552            if (cooked!=null) {
553              trans.concatenate(cooktransform);
554              view.draw.drawImage(cooked,trans,view.observer);
555  //        cooked.lock(false);
556            }
557            else if (cookable()) {
558              showOutline(view);
559            }
560            else view.draw.drawString(""+name+" not cooked",30,30);
561          }
562        }
563        else {
564  //        CacheRef.uses(view.parent,null);
565        }
566      }
567      catch (RuntimeException e) {
568        if (!exdone) {
569          exdone = true;
570          String str = "overall painting segment "+name+"\n";
571          ErrorLog.exception(e,str);
572  //      System.out.println(
573  //      e.printStackTrace();
574  //);
575          return;
576        }
577      }
578  
579    }
580  
581  //  transient AffineTransform oldbtrans = null;
582  //  transient Rectangle2D oldbounds = null;
583  
584    public java.awt.geom.Rectangle2D getBounds(java.awt.geom.AffineTransform trans) {
585      if (!cookable()) return(null);    //should return bounds of image
586      solve();
587  
588  //    if (oldbtrans!=null && trans.equals(oldbtrans)) return(oldbounds);
589  
590  //    Point2D.Double pos = new Point2D.Double((double)boundary.xpoints[0],(double)boundary.ypoints[0]);
591      Point2D.Double pos = new Point2D.Double();
592  
593  //    trans.transform(map.ftrans(pos,pos2),pos);
594      trans.transform(boundary.mapped[0],pos);
595  
596      Rectangle2D res = new Rectangle2D.Double(pos.getX(),pos.getY(),0.0,0.0);
597  
598      for (int i = 0; i<boundary.npoints;i++) {
599  //      pos.setLocation((double)boundary.xpoints[i],(double)boundary.ypoints[i]);
600        try {
601        trans.transform(boundary.mapped[i],pos);
602        } catch (RuntimeException exc) {System.out.println("Seg = "+name+" boundary = "+boundary+
603          						 " mapped = "+boundary.mapped+" len = "+boundary.mapped.length+
604          						 " i = "+i+" point = "+boundary.mapped[i]);}
605        res.add(pos);
606      }
607      
608  //    oldbtrans = (AffineTransform)trans.clone();
609  //    oldbounds = res;
610      return(res);
611    }
612  
613    public boolean isDirty(){
614      return(dirty);
615    }
616  
617    public int getLevel() {return(super.getLevel()+1);}
618  
619    public void getPropertyEdit(Object[] edits,int slot,java.util.Set sub,Symbol parent) {
620      JPanel pan = new JPanel();
621      edits[slot] = pan;
622      ((Component)edits[slot]).setName("Segment");
623      JButton but = new JButton(MORPH);
624      but.addActionListener(this);
625      pan.add(but);
626      but = new JButton(EDIT);
627      but.addActionListener(this);
628      pan.add(but);
629      super.getPropertyEdit(edits,slot-1,sub,parent);
630    }
631    
632    public void acceptPropertyEdit() {
633      super.acceptPropertyEdit();
634    }
635  
636    public void abandonPropertyEdit() {
637      super.abandonPropertyEdit();
638    }
639  
640    public void actionPerformed(ActionEvent e) {
641        String command = ((AbstractButton)e.getSource()).getActionCommand();
642  
643        if (command == EDIT)
644          CartoFrame.topframe.addEditor(this);
645        if (command == MORPH)
646          doCook(null);
647    }
648  
649    public void doCook(final SegEditor editor) {
650      map.solved = false;
651      if (cookondemand) {
652        map.overlaytrans = null;
653        boundary.mapped = null;
654        unCook();
655      }
656      else {
657      Carto.startBackground(
658        new Thread(){public void run() {
659          try {
660            cook(editor);
661          } catch (CookKillException ck) {
662          } finally {
663            Carto.cleanBackground();
664          }
665       }});
666      }
667    }
668  }
669