Example #1
0
 @SuppressWarnings("unchecked")
 private void updatePointList() {
   for (SmoothPathSegment sps : spsMap.values()) sps.remove();
   spsMap.clear();
   for (LinearPathSegment lps : lpsMap.values()) lps.remove();
   lpsMap.clear();
   if (arrow != null) arrow.remove();
   Set<PathPoint> pps = ((HashMap<PathPoint, PointVisual>) pvMap.clone()).keySet();
   int s = pvList.size();
   ActiveArrayList<PathPoint> pp = path.points;
   int s2 = pp.size();
   while (s > s2) pvList.remove(--s);
   pvList.ensureCapacity(s2);
   for (int i = 0; i < s2; i++) {
     PathPoint p = pp.get(i);
     PointVisual v = pvMap.get(p);
     if (v == null) {
       v = new PointVisual(p);
       pvMap.put(p, v);
     } else {
       pps.remove(p);
     }
     if (i >= s) pvList.add(v);
     else pvList.set(i, v);
   }
   if (pps.contains(properties.get(PPathEditor.SELECTED_POINT)))
     properties.put(PPathEditor.SELECTED_POINT, null);
   for (PathPoint pathPoint : pps) pvMap.remove(pathPoint).remove();
   if (path.get(PPath.SMOOTH)) {
     boolean closed = path.properties.get(PPath.CLOSED);
     if (s2 >= 3) {
       PathPoint[] rpp = new PathPoint[4];
       for (int i = 0; i < 4; i++) rpp[i] = pp.get(i % s2);
       int i2 = closed ? s2 + 1 : s2 - 2;
       for (int i = 1; i < i2; i++) {
         PathPoint p = rpp[1];
         spsMap.put(p, new SmoothPathSegment(rpp));
         rpp = Arrays.copyOfRange(rpp, 1, 5);
         rpp[3] = pp.get((i + 3) % s2);
       }
       if (!closed) {
         PathPoint p = pp.get(0);
         spsMap.put(p, new SmoothPathSegment(null, p, pp.get(1), pp.get(2)));
         p = pp.get(s2 - 2);
         spsMap.put(p, new SmoothPathSegment(pp.get(s2 - 3), p, pp.get(s2 - 1), null));
       }
       arrow = new PathArrow(spsMap.get(pp.get(0)));
     }
   } else {
     if (s2 >= 2) {
       for (int i = 0; i < s2 - 1; i++) {
         PathPoint p = path.points.get(i);
         lpsMap.put(p, new LinearPathSegment(p, path.points.get(i + 1)));
       }
       if (path.properties.get(PPath.CLOSED)) {
         PathPoint p = path.points.get(s2 - 1);
         lpsMap.put(p, new LinearPathSegment(p, path.points.get(0)));
       }
       arrow = new PathArrow(null);
     }
   }
 }
Example #2
0
 private void mouseEvent(MouseEvent e) {
   Point p = e.getPoint().getLocation();
   componentToVisual(p);
   int s = POINT_MOUSE_RANGE - POINT_SIZE / 2 + 1;
   Iterator<Visual> vi = binVisual.intersect(new Rectangle(p.x - s, p.y - s, 2 * s, 2 * s));
   PathPoint ppOver = null;
   int posd = POINT_MOUSE_RANGE * POINT_MOUSE_RANGE;
   while (vi.hasNext()) {
     Visual v = vi.next();
     if (v instanceof PointVisual) {
       PathPoint pp = ((PointVisual) v).point;
       int xd = pp.getX() - p.x;
       int yd = pp.getY() - p.y;
       int sd = xd * xd + yd * yd;
       if (sd <= posd) {
         ppOver = pp;
         posd = sd;
         break;
       }
     }
   }
   switch (e.getID()) {
     case MouseEvent.MOUSE_PRESSED:
       ppPressed = ppOver;
       switch (e.getButton()) {
         case MouseEvent.BUTTON1:
           if (ppOver == null) {
             PathPoint pp = properties.get(PPathEditor.SELECTED_POINT);
             ppOver = new PathPoint(p.x, p.y, pp == null ? 100 : pp.getSpeed());
             if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0)
               movePathPoint(ppOver, p.x, p.y, true);
             path.points.add(ppOver);
             ppPressed = ppOver;
           } else if ((e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0) {
             int i = path.points.indexOf(ppOver);
             ppOver = new PathPoint(ppOver.getX(), ppOver.getY(), ppOver.getSpeed());
             path.points.add(i, ppOver);
             ppPressed = ppOver;
           }
           properties.put(PPathEditor.SELECTED_POINT, ppOver);
           dragging = true;
           dragOffset = p.getLocation();
           dragOffset.translate(-ppOver.getX(), -ppOver.getY());
           break;
       }
       break;
     case MouseEvent.MOUSE_DRAGGED:
       if (dragging) {
         lockBounds();
         PathPoint pp = properties.get(PPathEditor.SELECTED_POINT);
         if (pp == null) {
           ppPressed = null;
           dragging = false;
           unlockBounds();
           break;
         }
         movePathPoint(
             pp,
             p.x - dragOffset.x,
             p.y - dragOffset.y,
             (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0);
       } else if (ppPressed != ppOver) ppPressed = null;
       break;
     case MouseEvent.MOUSE_RELEASED:
       switch (e.getButton()) {
         case MouseEvent.BUTTON1:
           dragging = false;
           unlockBounds();
           break;
         case MouseEvent.BUTTON3:
           if (dragging) {
             dragging = false;
             unlockBounds();
           } else if (ppPressed != null) path.points.remove(ppPressed);
           break;
       }
       ppPressed = null;
   }
 }
Example #3
0
public class PathEditor extends VisualPanel implements UpdateListener {
  private static final int ROOM_LAYER = -1;
  private static final int BIN_LAYER = 0;
  private static final int GRID_LAYER = 1;
  private static final int POINT_SIZE = 8;
  private static final int POINT_MOUSE_RANGE = 8;
  private static final int ARROW_SIZE = 10;
  private static final int LINE_WIDTH = 5;
  private static final long serialVersionUID = 1L;
  private final Path path;
  private RoomVisual roomVisual;
  private final BinVisual binVisual;
  private final GridVisual gridVisual;
  private final PathPropertyListener ppl = new PathPropertyListener();
  private final PointListListener pll = new PointListListener();

  public final PropertyMap<PPathEditor> properties;

  private final PathEditorPropertyValidator pepv = new PathEditorPropertyValidator();

  public enum PPathEditor {
    SHOW_GRID,
    SELECTED_POINT
  }

  private static final EnumMap<PPathEditor, Object> DEFS =
      PropertyMap.makeDefaultMap(PPathEditor.class, true, null);

  private final ArrayList<PointVisual> pvList;
  private final HashMap<PathPoint, PointVisual> pvMap;

  private PathArrow arrow;

  public PathEditor(Path p) {
    enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    properties = new PropertyMap<PPathEditor>(PPathEditor.class, pepv, DEFS);
    binVisual = new BinVisual(container, 128, 512, 512);
    put(BIN_LAYER, binVisual);
    int sx = p.get(PPath.SNAP_X);
    int sy = p.get(PPath.SNAP_Y);
    gridVisual = new GridVisual(false, sx, sy);
    put(GRID_LAYER, gridVisual);
    path = p;
    path.reference.updateSource.addListener(this);
    path.properties.updateSource.addListener(ppl);
    path.points.updateSource.addListener(pll);
    int s = path.points.size();
    pvList = new ArrayList<PointVisual>(s);
    pvMap = new HashMap<PathPoint, PointVisual>(Math.max((int) (s / .75f) + 1, 16));
    updatePointList();
    ResourceReference<Room> r = path.get(PPath.BACKGROUND_ROOM);
    setRoom(r == null ? null : r.get());
  }

  public void updated(UpdateEvent e) {
    invalidate();
    repaint();
  }

  private boolean dragging = false;
  private Point dragOffset;
  private PathPoint ppPressed;

  private void mouseEvent(MouseEvent e) {
    Point p = e.getPoint().getLocation();
    componentToVisual(p);
    int s = POINT_MOUSE_RANGE - POINT_SIZE / 2 + 1;
    Iterator<Visual> vi = binVisual.intersect(new Rectangle(p.x - s, p.y - s, 2 * s, 2 * s));
    PathPoint ppOver = null;
    int posd = POINT_MOUSE_RANGE * POINT_MOUSE_RANGE;
    while (vi.hasNext()) {
      Visual v = vi.next();
      if (v instanceof PointVisual) {
        PathPoint pp = ((PointVisual) v).point;
        int xd = pp.getX() - p.x;
        int yd = pp.getY() - p.y;
        int sd = xd * xd + yd * yd;
        if (sd <= posd) {
          ppOver = pp;
          posd = sd;
          break;
        }
      }
    }
    switch (e.getID()) {
      case MouseEvent.MOUSE_PRESSED:
        ppPressed = ppOver;
        switch (e.getButton()) {
          case MouseEvent.BUTTON1:
            if (ppOver == null) {
              PathPoint pp = properties.get(PPathEditor.SELECTED_POINT);
              ppOver = new PathPoint(p.x, p.y, pp == null ? 100 : pp.getSpeed());
              if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0)
                movePathPoint(ppOver, p.x, p.y, true);
              path.points.add(ppOver);
              ppPressed = ppOver;
            } else if ((e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0) {
              int i = path.points.indexOf(ppOver);
              ppOver = new PathPoint(ppOver.getX(), ppOver.getY(), ppOver.getSpeed());
              path.points.add(i, ppOver);
              ppPressed = ppOver;
            }
            properties.put(PPathEditor.SELECTED_POINT, ppOver);
            dragging = true;
            dragOffset = p.getLocation();
            dragOffset.translate(-ppOver.getX(), -ppOver.getY());
            break;
        }
        break;
      case MouseEvent.MOUSE_DRAGGED:
        if (dragging) {
          lockBounds();
          PathPoint pp = properties.get(PPathEditor.SELECTED_POINT);
          if (pp == null) {
            ppPressed = null;
            dragging = false;
            unlockBounds();
            break;
          }
          movePathPoint(
              pp,
              p.x - dragOffset.x,
              p.y - dragOffset.y,
              (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0);
        } else if (ppPressed != ppOver) ppPressed = null;
        break;
      case MouseEvent.MOUSE_RELEASED:
        switch (e.getButton()) {
          case MouseEvent.BUTTON1:
            dragging = false;
            unlockBounds();
            break;
          case MouseEvent.BUTTON3:
            if (dragging) {
              dragging = false;
              unlockBounds();
            } else if (ppPressed != null) path.points.remove(ppPressed);
            break;
        }
        ppPressed = null;
    }
  }

  private void movePathPoint(PathPoint pp, int x, int y, boolean snap) {
    if (snap) {
      int sx = path.get(PPath.SNAP_X);
      int sy = path.get(PPath.SNAP_Y);
      pp.setX(negDiv(x + sx / 2, sx) * sx);
      pp.setY(negDiv(y + sy / 2, sy) * sy);
    } else {
      pp.setX(x);
      pp.setY(y);
    }
  }

  @Override
  protected void processMouseMotionEvent(MouseEvent e) {
    mouseEvent(e);
    super.processMouseMotionEvent(e);
  }

  @Override
  protected void processMouseEvent(MouseEvent e) {
    mouseEvent(e);
    super.processMouseEvent(e);
  }

  static final int HPS = POINT_SIZE >> 1;
  final BufferedImage[] pointImage = new BufferedImage[2];

  private class PointVisual extends PathVisual {
    final PathPoint point;
    final Rectangle bounds = new Rectangle(POINT_SIZE, POINT_SIZE);
    final PointPositionListener ppl = new PointPositionListener();
    private boolean selected;

    public PointVisual(PathPoint p) {
      point = p;
      binVisual.setDepth(this, 0);
      p.properties.getUpdateSource(PPathPoint.X).addListener(ppl);
      p.properties.getUpdateSource(PPathPoint.Y).addListener(ppl);
      validate();
    }

    public void setSelected(boolean s) {
      selected = s;
      binVisual.setDepth(this, s ? -2 : 0);
    }

    protected void calculateBounds() {
      bounds.setLocation(point.getX() - HPS, point.getY() - HPS);
    }

    protected void validate() {
      calculateBounds();
      setBounds(bounds);
    }

    public void paint(Graphics g) {
      int i = selected ? 1 : 0;
      if (pointImage[i] == null) {
        pointImage[i] =
            getGraphicsConfiguration()
                .createCompatibleImage(POINT_SIZE, POINT_SIZE, Transparency.BITMASK);
        Graphics2D g2 = pointImage[i].createGraphics();
        g2.setColor(selected ? Color.RED : Color.BLUE);
        g2.fillOval(0, 0, POINT_SIZE - 1, POINT_SIZE - 1);
        g2.setColor(Color.BLACK);
        g2.drawOval(0, 0, POINT_SIZE - 1, POINT_SIZE - 1);
      }
      g.drawImage(pointImage[i], 0, 0, null);
    }

    private class PointPositionListener extends PropertyUpdateListener<PPathPoint> {
      @Override
      public void updated(PropertyUpdateEvent<PPathPoint> e) {
        invalidate();
      }
    }
  }

  static final int HLW = LINE_WIDTH + 1 >> 1;

  static final Stroke STROKE_OUTER =
      new BasicStroke(LINE_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
  static final Stroke STROKE_INNER =
      new BasicStroke(LINE_WIDTH - 2.83f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);

  private int spPrecision;
  private float[] spBlending;

  private float getBlending(int p, int t) {
    if (p != spPrecision) {
      spPrecision = p;
      spBlending = new float[1 + 3 * p];
      float isp = 1f / (p * p);
      float ip = 1f / p;
      for (int i = 0; i < p; i++) spBlending[i] = i * i * isp * 0.5f;
      for (int i = p; i < 2 * p; i++) spBlending[i] = (-2 * i * i * isp + 6 * i * ip - 3) * 0.5f;
      for (int i = 2 * p; i <= 3 * p; i++) spBlending[i] = (i * i * isp - 6 * i * ip + 9) * 0.5f;
    }
    return spBlending[t];
  }

  private class LinearPathSegment extends PathVisual {
    final PathPoint[] pp = new PathPoint[2];
    final Rectangle bounds = new Rectangle();
    final PointPositionListener ppl = new PointPositionListener();
    int px0, py0, px1, py1;

    public LinearPathSegment(PathPoint... p) {
      if (p.length != 2) throw new IllegalArgumentException();
      System.arraycopy(p, 0, pp, 0, 2);
      binVisual.setDepth(this, 1);
      for (PathPoint point : p) {
        point.properties.getUpdateSource(PPathPoint.X).addListener(ppl);
        point.properties.getUpdateSource(PPathPoint.Y).addListener(ppl);
      }
      validate();
    }

    protected void calculateBounds() {
      px0 = pp[0].getX();
      py0 = pp[0].getY();
      px1 = pp[1].getX();
      py1 = pp[1].getY();
      bounds.setBounds(0, 0, -1, -1);
      bounds.add(px0, py0);
      bounds.add(px1, py1);
      bounds.grow(HLW, HLW);
      px0 -= bounds.x;
      py0 -= bounds.y;
      px1 -= bounds.x;
      py1 -= bounds.y;
    }

    @Override
    protected void validate() {
      calculateBounds();
      setBounds(bounds);
    }

    public void paint(Graphics g) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(Color.BLACK);
      g2.setStroke(STROKE_OUTER);
      g2.drawLine(px0, py0, px1, py1);
      g2.setColor(Color.WHITE);
      g2.setStroke(STROKE_INNER);
      g2.drawLine(px0, py0, px1, py1);
    }

    private class PointPositionListener extends PropertyUpdateListener<PPathPoint> {
      @Override
      public void updated(PropertyUpdateEvent<PPathPoint> e) {
        invalidate();
      }
    }
  }

  private class SmoothPathSegment extends PathVisual {
    final PathPoint[] pp = new PathPoint[4];
    final Rectangle bounds = new Rectangle();
    final PointPositionListener ppl = new PointPositionListener();
    final InnerSegment innerSegment = new InnerSegment();
    int[] px, py;

    public SmoothPathSegment(PathPoint... p) {
      if (p.length != 4) throw new IllegalArgumentException();
      System.arraycopy(p, 0, pp, 0, 4);
      binVisual.setDepth(this, 2);
      for (PathPoint point : p) {
        if (point == null) continue;
        point.properties.getUpdateSource(PPathPoint.X).addListener(ppl);
        point.properties.getUpdateSource(PPathPoint.Y).addListener(ppl);
      }
      validate();
    }

    @Override
    public void remove() {
      innerSegment.remove();
      super.remove();
    }

    private void pBlend(int i, int n, int t, int[] x, int[] y) {
      float w0 = getBlending(n, t + 1 + 2 * n);
      float w2 = getBlending(n, t + 1);
      px[i] = x[1] + Math.round(w0 * (x[0] - x[1]) + w2 * (x[2] - x[1]));
      py[i] = y[1] + Math.round(w0 * (y[0] - y[1]) + w2 * (y[2] - y[1]));
    }

    protected void calculateBounds() {
      bounds.setBounds(0, 0, -1, -1);
      int[] x = new int[4];
      int[] y = new int[4];
      for (int i = 0; i < 4; i++) {
        if (pp[i] == null) continue;
        x[i] = pp[i].getX();
        y[i] = pp[i].getY();
      }
      int p = path.properties.get(PPath.PRECISION);
      int n = (1 << p);
      if (pp[0] == null) {
        px = new int[2];
        py = new int[2];
        px[0] = x[1];
        py[0] = y[1];
        bounds.add(x[1], y[1]);
        pBlend(1, n, 0, Arrays.copyOfRange(x, 1, 4), Arrays.copyOfRange(y, 1, 4));
        bounds.add(px[1], py[1]);
      } else {
        px = new int[n];
        py = new int[n];
        for (int t = 0; t < n - 1; t++) {
          pBlend(t, n, t, x, y);
          bounds.add(px[t], py[t]);
        }
        if (pp[3] == null) {
          px[n - 1] = x[2];
          py[n - 1] = y[2];
        } else pBlend(n - 1, n, 0, Arrays.copyOfRange(x, 1, 4), Arrays.copyOfRange(y, 1, 4));
        bounds.add(px[n - 1], py[n - 1]);
      }
      bounds.grow(HLW, HLW);
      for (int t = 0; t < px.length; t++) {
        px[t] -= bounds.x;
        py[t] -= bounds.y;
      }
    }

    @Override
    protected void validate() {
      calculateBounds();
      setBounds(bounds);
      innerSegment.setBounds(bounds);
    }

    public void paint(Graphics g) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(Color.BLACK);
      g2.setStroke(STROKE_OUTER);
      g2.drawPolyline(px, py, px.length);
    }

    private class InnerSegment extends VisualBox {
      public InnerSegment() {
        super(binVisual);
        binVisual.setDepth(this, 1);
      }

      @Override
      protected void setBounds(Rectangle b) {
        super.setBounds(b);
      }

      public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(Color.WHITE);
        g2.setStroke(STROKE_INNER);
        g2.drawPolyline(px, py, px.length);
      }
    }

    private class PointPositionListener extends PropertyUpdateListener<PPathPoint> {
      @Override
      public void updated(PropertyUpdateEvent<PPathPoint> e) {
        invalidate();
      }
    }
  }

  private static final double ARROW_ANGLE = 2 * Math.PI / 3;

  private class PathArrow extends PathVisual {
    final PointPositionListener ppl = new PointPositionListener();
    final SmoothPathSegment segment;
    final int[] px = new int[4];
    final int[] py = new int[4];

    public PathArrow(SmoothPathSegment s) {
      segment = s;
      binVisual.setDepth(this, -1);
      int i2 = s == null ? 2 : path.get(PPath.CLOSED) ? 4 : 3;
      for (int i = 0; i < i2; i++) {
        PathPoint p = path.points.get(i == 3 ? path.points.size() - 1 : i);
        p.properties.getUpdateSource(PPathPoint.X).addListener(ppl);
        p.properties.getUpdateSource(PPathPoint.Y).addListener(ppl);
      }
      validate();
    }

    private void calculatePoints(int x, int y, double d) {
      px[0] = x + (int) Math.round(ARROW_SIZE * Math.cos(d));
      py[0] = y - (int) Math.round(ARROW_SIZE * Math.sin(d));
      px[1] = x + (int) Math.round(ARROW_SIZE * Math.cos(d + ARROW_ANGLE));
      py[1] = y - (int) Math.round(ARROW_SIZE * Math.sin(d + ARROW_ANGLE));
      px[2] = x;
      py[2] = y;
      px[3] = x + (int) Math.round(ARROW_SIZE * Math.cos(d - ARROW_ANGLE));
      py[3] = y - (int) Math.round(ARROW_SIZE * Math.sin(d - ARROW_ANGLE));
    }

    private int sqrdist(int x, int y) {
      return x * x + y * y;
    }

    @Override
    protected void validate() {
      if (segment == null) {
        PathPoint p = path.points.get(0);
        PathPoint p2 = path.points.get(1);
        int x = p.getX();
        int y = p.getY();
        calculatePoints(x, y, Math.atan2(y - p2.getY(), p2.getX() - x));
      } else {
        segment.validate();
        if (path.get(PPath.CLOSED)) {
          int i = segment.px.length - 1;
          int x = segment.px[i];
          int y = segment.py[i];
          int i2 = i - 1;
          while (i2 > 0 && sqrdist(segment.px[i2] - x, segment.py[i2] - y) < 4) i2--;
          calculatePoints(
              x + segment.bounds.x,
              y + segment.bounds.y,
              Math.atan2(segment.py[i2] - y, x - segment.px[i2]));
        } else {
          int x = segment.px[0];
          int y = segment.py[0];
          int i = 1;
          while (i < segment.px.length - 1 && sqrdist(segment.px[i] - x, segment.py[i] - y) < 4)
            i++;
          calculatePoints(
              x + segment.bounds.x,
              y + segment.bounds.y,
              Math.atan2(y - segment.py[i], segment.px[i] - x));
        }
      }
      Rectangle bounds = new Rectangle(-1, -1);
      for (int i = 0; i < 4; i++) bounds.add(px[i], py[i]);
      for (int i = 0; i < 4; i++) {
        px[i] -= bounds.x;
        py[i] -= bounds.y;
      }
      setBounds(bounds);
    }

    public void paint(Graphics g) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(new Color(0, 224, 0));
      g2.fillPolygon(px, py, 4);
      g2.setColor(Color.BLACK);
      g2.drawPolygon(px, py, 4);
    }

    private class PointPositionListener extends PropertyUpdateListener<PPathPoint> {
      @Override
      public void updated(PropertyUpdateEvent<PPathPoint> e) {
        invalidate();
      }
    }
  }

  private abstract class PathVisual extends VisualBox {
    private boolean invalid;

    public PathVisual() {
      super(binVisual);
    }

    protected abstract void validate();

    protected final void invalidate() {
      if (invalid) return;
      invalid = true;
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              try {
                if (invalid) validate();
              } finally {
                invalid = false;
              }
            }
          });
    }
  }

  final HashMap<PathPoint, SmoothPathSegment> spsMap = new HashMap<PathPoint, SmoothPathSegment>();
  final HashMap<PathPoint, LinearPathSegment> lpsMap = new HashMap<PathPoint, LinearPathSegment>();

  @SuppressWarnings("unchecked")
  private void updatePointList() {
    for (SmoothPathSegment sps : spsMap.values()) sps.remove();
    spsMap.clear();
    for (LinearPathSegment lps : lpsMap.values()) lps.remove();
    lpsMap.clear();
    if (arrow != null) arrow.remove();
    Set<PathPoint> pps = ((HashMap<PathPoint, PointVisual>) pvMap.clone()).keySet();
    int s = pvList.size();
    ActiveArrayList<PathPoint> pp = path.points;
    int s2 = pp.size();
    while (s > s2) pvList.remove(--s);
    pvList.ensureCapacity(s2);
    for (int i = 0; i < s2; i++) {
      PathPoint p = pp.get(i);
      PointVisual v = pvMap.get(p);
      if (v == null) {
        v = new PointVisual(p);
        pvMap.put(p, v);
      } else {
        pps.remove(p);
      }
      if (i >= s) pvList.add(v);
      else pvList.set(i, v);
    }
    if (pps.contains(properties.get(PPathEditor.SELECTED_POINT)))
      properties.put(PPathEditor.SELECTED_POINT, null);
    for (PathPoint pathPoint : pps) pvMap.remove(pathPoint).remove();
    if (path.get(PPath.SMOOTH)) {
      boolean closed = path.properties.get(PPath.CLOSED);
      if (s2 >= 3) {
        PathPoint[] rpp = new PathPoint[4];
        for (int i = 0; i < 4; i++) rpp[i] = pp.get(i % s2);
        int i2 = closed ? s2 + 1 : s2 - 2;
        for (int i = 1; i < i2; i++) {
          PathPoint p = rpp[1];
          spsMap.put(p, new SmoothPathSegment(rpp));
          rpp = Arrays.copyOfRange(rpp, 1, 5);
          rpp[3] = pp.get((i + 3) % s2);
        }
        if (!closed) {
          PathPoint p = pp.get(0);
          spsMap.put(p, new SmoothPathSegment(null, p, pp.get(1), pp.get(2)));
          p = pp.get(s2 - 2);
          spsMap.put(p, new SmoothPathSegment(pp.get(s2 - 3), p, pp.get(s2 - 1), null));
        }
        arrow = new PathArrow(spsMap.get(pp.get(0)));
      }
    } else {
      if (s2 >= 2) {
        for (int i = 0; i < s2 - 1; i++) {
          PathPoint p = path.points.get(i);
          lpsMap.put(p, new LinearPathSegment(p, path.points.get(i + 1)));
        }
        if (path.properties.get(PPath.CLOSED)) {
          PathPoint p = path.points.get(s2 - 1);
          lpsMap.put(p, new LinearPathSegment(p, path.points.get(0)));
        }
        arrow = new PathArrow(null);
      }
    }
  }

  private static final EnumSet<Show> ROOM_SHOW =
      EnumSet.of(Show.BACKGROUNDS, Show.INSTANCES, Show.TILES, Show.FOREGROUNDS);

  private void setRoom(Room r) {
    if (roomVisual == null ? r == null : roomVisual.room == r) return;
    roomVisual = r == null ? null : new RoomVisual(container, r, ROOM_SHOW);
    put(ROOM_LAYER, roomVisual);
  }

  private class PathPropertyListener extends PropertyUpdateListener<PPath> {
    @Override
    public void updated(PropertyUpdateEvent<PPath> e) {
      switch (e.key) {
        case SNAP_X:
          gridVisual.setWidth((Integer) path.get(PPath.SNAP_X));
          repaint();
          break;
        case SNAP_Y:
          gridVisual.setHeight((Integer) path.get(PPath.SNAP_Y));
          repaint();
          break;
        case PRECISION:
          for (SmoothPathSegment s : spsMap.values()) s.validate();
          arrow.validate();
          break;
        case CLOSED:
        case SMOOTH:
          // TODO: Optimize
          updatePointList();
          break;
        case BACKGROUND_ROOM:
          ResourceReference<Room> r = path.get(PPath.BACKGROUND_ROOM);
          setRoom(r == null ? null : r.get());
      }
    }
  }

  private class PointListListener implements UpdateListener {
    public void updated(UpdateEvent e) {
      ListUpdateEvent lue = (ListUpdateEvent) e;
      switch (lue.type) {
        case ADDED:
          //					for (int i = lue.fromIndex; i <= lue.toIndex; i++)
          //						{
          //						PathPoint p = path.points.get(i);
          //						PointVisual v = new PointVisual(p);
          //						pvMap.put(p,v);
          //						pvList.add(i,v);
          //						}
          //					break;
        case REMOVED:
          //					for (int i = lue.toIndex; i <= lue.fromIndex; i++)
          //						{
          //						PathPoint p = path.points.get(i);
          //						pvMap.remove(p);
          //						pvList.remove(i).box.remove();
          //						}
          //					break;
        case CHANGED:
          updatePointList();
      }
    }
  }

  private class PathEditorPropertyValidator implements PropertyValidator<PPathEditor> {
    public Object validate(PPathEditor k, Object v) {
      switch (k) {
        case SELECTED_POINT:
          PointVisual pv = pvMap.get(properties.get(k));
          if (pv != null) {
            if (v == pv.point) break;
            pv.setSelected(false);
          }
          pv = pvMap.get(v);
          if (pv == null) {
            if (pvList.size() < 1) return null;
            pv = pvList.get(0);
          }
          pv.setSelected(true);
          return pv.point;
        case SHOW_GRID:
          if (v instanceof Boolean) put(GRID_LAYER, (Boolean) v ? gridVisual : null);
          else return properties.get(k);
          break;
      }
      return v;
    }
  }
}
public class Background extends Resource<Background, Background.PBackground> {
  private BufferedImage backgroundImage = null;
  private SoftReference<BufferedImage> imageCache = null;

  public enum PBackground {
    TRANSPARENT,
    SMOOTH_EDGES,
    PRELOAD,
    USE_AS_TILESET,
    TILE_WIDTH,
    TILE_HEIGHT,
    H_OFFSET,
    V_OFFSET,
    H_SEP,
    V_SEP
  }

  private static final EnumMap<PBackground, Object> DEFS =
      PropertyMap.makeDefaultMap(PBackground.class, false, false, false, false, 16, 16, 0, 0, 0, 0);

  public Background() {
    this(null);
  }

  public Background(ResourceReference<Background> r) {
    super(r);
    setName(Prefs.prefixes.get(Kind.BACKGROUND));
  }

  public Background makeInstance(ResourceReference<Background> r) {
    return new Background(r);
  }

  public BufferedImage getDisplayImage() {
    if (backgroundImage == null) return null;
    BufferedImage bi;
    if (imageCache != null) {
      bi = imageCache.get();
      if (bi != null) {
        return bi;
      }
    }
    bi = backgroundImage;
    if (get(PBackground.TRANSPARENT)) bi = Util.getTransparentIcon(bi);
    imageCache = new SoftReference<BufferedImage>(bi);
    return bi;
  }

  protected void postCopy(Background dest) {
    dest.backgroundImage = Util.cloneImage(backgroundImage);
  }

  public Kind getKind() {
    return Kind.BACKGROUND;
  }

  @Override
  protected void fireUpdate() {
    if (imageCache != null) imageCache.clear();
    super.fireUpdate();
  }

  public BufferedImage getBackgroundImage() {
    return backgroundImage;
  }

  public void setBackgroundImage(BufferedImage backgroundImage) {
    this.backgroundImage = backgroundImage;
    fireUpdate();
  }

  public int getWidth() {
    return backgroundImage == null ? 0 : backgroundImage.getWidth();
  }

  public int getHeight() {
    return backgroundImage == null ? 0 : backgroundImage.getHeight();
  }

  @Override
  protected PropertyMap<PBackground> makePropertyMap() {
    return new PropertyMap<PBackground>(PBackground.class, this, DEFS);
  }
}