Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画するには java.awt.geom.Area クラスを使えば解決。

こんな図形を想定。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

ソースコード。


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
 
public class DonutPolygon {
 
  public static void main(String[] args) throws Exception {
    
    Polygon out1 = new Polygon(new int[]{60,180,240,180,30}, new int[]{60,30,90,240,150}, 5);
    Polygon in1 = new Polygon(new int[]{120,150,180,150,120}, new int[]{60,60,90,120,90}, 5);
    Polygon in2 = new Polygon(new int[]{60,120,120,150,150,90,90,60}, new int[]{120,120,150,150,180,180,150,150}, 8);
 
    Area area = new Area();
    area.add(new Area(out1));
    area.subtract(new Area(in1));
    area.subtract(new Area(in2));
 
    // ついでに PathIterator がどうなっているのか調べる
    printPathIterator(area.getPathIterator(new AffineTransform()));
 
    BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    g.setPaint(Color.BLUE);
    g.fill(area);
    g.setStroke(new BasicStroke(5.0f));
    g.setPaint(Color.RED);
    g.draw(area);
    g.dispose();
    
    ImageIO.write(image, "png", new File("donut_polygon.png"));
  }
 
  private static void printPathIterator(PathIterator pi) {
 
    switch (pi.getWindingRule()) {
    case PathIterator.WIND_EVEN_ODD:
      System.out.println("WindingRule: WIND_EVEN_ODD");
      break;
    case PathIterator.WIND_NON_ZERO:
      System.out.println("WindingRule: WIND_NON_ZERO");
      break;
    }
 
    while (!pi.isDone()) {
      double coords[] = new double[6];
      int type = pi.currentSegment(coords);
      switch (type) {
      case PathIterator.SEG_MOVETO:
        System.out.print("SEG_MOVETO: ");
        break;
      case PathIterator.SEG_CLOSE:
        System.out.print("SEG_CLOSE: ");
        break;
      case PathIterator.SEG_LINETO:
        System.out.print("SEG_LINETO: ");
        break;
      case PathIterator.SEG_CUBICTO:
        System.out.print("SEG_CUBICTO: ");
        break;
      case PathIterator.SEG_QUADTO:
        System.out.print("SEG_QUADTO: ");
        break;
      }
      System.out.println(
        "(" + coords[0] + "," + coords[1] + ") " +
        "(" + coords[2] + "," + coords[3] + ") " +
        "(" + coords[4] + "," + coords[5] + ")");
      pi.next();
    }
  }
}

出力された画像。ちゃんとドーナツポリゴンとして描画された。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

Java でドーナツポリゴンを描画するなら Area クラスを使えば OK。

ちなみに、 PathIterator がどうなっているか調べてみたら、指定したものと点の順番がちがう。


WindingRule: WIND_NON_ZERO
SEG_MOVETO: (150.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (120.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (180.0,30.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (30.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,240.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (240.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,30.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)

以下、いろいろ試してみた蛇足 (何も解決はしていない・・・)。

検証1: GeneralPath でドーナツ・ポリゴンを試す

最初のサンプルで指定したのと同じ点の順序で GeneralPath を構築して描画してみたら失敗。

出力された画像。ドーナツの穴ができない。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

ソースコード:


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
 
public class DonutPolygon {
 
  public static void main(String[] args) throws Exception {
    
    GeneralPath p = new GeneralPath(GeneralPath.WIND_NON_ZERO);
    // 外側
    p.moveTo(60, 60);
    p.lineTo(180, 30);
    p.lineTo(240, 90);
    p.lineTo(180, 240);
    p.lineTo(30, 150);
    p.closePath();
    // 内側
    p.moveTo(120, 60);
    p.lineTo(150, 60);
    p.lineTo(180, 90);
    p.lineTo(150, 120);
    p.lineTo(120, 90);
    p.closePath();
    // 内側
    p.moveTo(60, 120);
    p.lineTo(120, 120);
    p.lineTo(120, 150);
    p.lineTo(150, 150);
    p.lineTo(150, 180);
    p.lineTo(90, 180);
    p.lineTo(90, 150);
    p.lineTo(60, 150);
    p.closePath();
 
    printPathIterator(p.getPathIterator(new AffineTransform()));
    
    BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    g.setPaint(Color.BLUE);
    g.fill(p);
    g.setStroke(new BasicStroke(5.0f));
    g.setPaint(Color.RED);
    g.draw(p);
    g.dispose();
    
    ImageIO.write(image, "png", new File("donut_polygon_2.png"));
  }
 
  private static void printPathIterator(PathIterator pi) {
 
    switch (pi.getWindingRule()) {
    case PathIterator.WIND_EVEN_ODD:
      System.out.println("WindingRule: WIND_EVEN_ODD");
      break;
    case PathIterator.WIND_NON_ZERO:
      System.out.println("WindingRule: WIND_NON_ZERO");
      break;
    }
 
    while (!pi.isDone()) {
      double coords[] = new double[6];
      int type = pi.currentSegment(coords);
      switch (type) {
      case PathIterator.SEG_MOVETO:
        System.out.print("SEG_MOVETO: ");
        break;
      case PathIterator.SEG_CLOSE:
        System.out.print("SEG_CLOSE: ");
        break;
      case PathIterator.SEG_LINETO:
        System.out.print("SEG_LINETO: ");
        break;
      case PathIterator.SEG_CUBICTO:
        System.out.print("SEG_CUBICTO: ");
        break;
      case PathIterator.SEG_QUADTO:
        System.out.print("SEG_QUADTO: ");
        break;
      }
      System.out.println(
        "(" + coords[0] + "," + coords[1] + ") " +
        "(" + coords[2] + "," + coords[3] + ") " +
        "(" + coords[4] + "," + coords[5] + ")");
      pi.next();
    }
  }
}

PathIterator の状態:


WindingRule: WIND_NON_ZERO
SEG_MOVETO: (60.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,30.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (240.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,240.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (30.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (120.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (60.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)

検証2: Area オブジェクトが出力する PathIterator を参考にする

最初のサンプルで出力された PathIterator と同じ点の順序で GeneralPath を構築して描画してみたら成功。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

ってことは、点の順番が問題だったってこと?

ソースコード:


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
 
public class DonutPolygon {
 
  public static void main(String[] args) throws Exception {
    
    GeneralPath p = new GeneralPath(GeneralPath.WIND_NON_ZERO);
    // 内側
    p.moveTo(150.0,60.0);
    p.lineTo(180.0,90.0);
    p.lineTo(150.0,120.0);
    p.lineTo(120.0,90.0);
    p.lineTo(120.0,60.0);
    p.closePath();
    // 内側
    p.moveTo(120.0,120.0);
    p.lineTo(120.0,150.0);
    p.lineTo(150.0,150.0);
    p.lineTo(150.0,180.0);
    p.lineTo(90.0,180.0);
    p.lineTo(90.0,150.0);
    p.lineTo(60.0,150.0);
    p.lineTo(60.0,120.0);
    p.closePath();
    // 外側
    p.moveTo(180.0,30.0);
    p.lineTo(60.0,60.0);
    p.lineTo(30.0,150.0);
    p.lineTo(180.0,240.0);
    p.lineTo(240.0,90.0);
    p.lineTo(180.0,30.0);
    p.closePath();
 
    printPathIterator(p.getPathIterator(new AffineTransform()));
    
    BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    g.setPaint(Color.BLUE);
    g.fill(p);
    g.setStroke(new BasicStroke(5.0f));
    g.setPaint(Color.RED);
    g.draw(p);
    g.dispose();
    
    ImageIO.write(image, "png", new File("donut_polygon_3.png"));
  }
 
  private static void printPathIterator(PathIterator pi) {
 
    switch (pi.getWindingRule()) {
    case PathIterator.WIND_EVEN_ODD:
      System.out.println("WindingRule: WIND_EVEN_ODD");
      break;
    case PathIterator.WIND_NON_ZERO:
      System.out.println("WindingRule: WIND_NON_ZERO");
      break;
    }
 
    while (!pi.isDone()) {
      double coords[] = new double[6];
      int type = pi.currentSegment(coords);
      switch (type) {
      case PathIterator.SEG_MOVETO:
        System.out.print("SEG_MOVETO: ");
        break;
      case PathIterator.SEG_CLOSE:
        System.out.print("SEG_CLOSE: ");
        break;
      case PathIterator.SEG_LINETO:
        System.out.print("SEG_LINETO: ");
        break;
      case PathIterator.SEG_CUBICTO:
        System.out.print("SEG_CUBICTO: ");
        break;
      case PathIterator.SEG_QUADTO:
        System.out.print("SEG_QUADTO: ");
        break;
      }
      System.out.println(
        "(" + coords[0] + "," + coords[1] + ") " +
        "(" + coords[2] + "," + coords[3] + ") " +
        "(" + coords[4] + "," + coords[5] + ")");
      pi.next();
    }
  }
}

PathIterator の状態:


WindingRule: WIND_NON_ZERO
SEG_MOVETO: (150.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (120.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (120.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (150.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,180.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (90.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,120.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)
SEG_MOVETO: (180.0,30.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (60.0,60.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (30.0,150.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,240.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (240.0,90.0) (0.0,0.0) (0.0,0.0)
SEG_LINETO: (180.0,30.0) (0.0,0.0) (0.0,0.0)
SEG_CLOSE: (0.0,0.0) (0.0,0.0) (0.0,0.0)

検証3: Area オブジェクトが出力する PathIterator の点の起点を調べる

見た感じ一番上(最小のy値)の中で一番右(最大のx値)が起点になってる感じ。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

ソースコード:


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
 
public class DonutPolygon {
 
  public static void main(String[] args) throws Exception {
    
    GeneralPath p = new GeneralPath(GeneralPath.WIND_NON_ZERO);
    // 外側
    p.moveTo(150.0,60.0);
    p.lineTo(180.0,90.0);
    p.lineTo(150.0,120.0);
    p.lineTo(120.0,90.0);
    p.lineTo(120.0,60.0);
    p.closePath();
    // 内側
    p.moveTo(120.0,120.0);
    p.lineTo(120.0,150.0);
    p.lineTo(150.0,150.0);
    p.lineTo(150.0,180.0);
    p.lineTo(90.0,180.0);
    p.lineTo(90.0,150.0);
    p.lineTo(60.0,150.0);
    p.lineTo(60.0,120.0);
    p.closePath();
    // 内側
    p.moveTo(180.0,30.0);
    p.lineTo(60.0,60.0);
    p.lineTo(30.0,150.0);
    p.lineTo(180.0,240.0);
    p.lineTo(240.0,90.0);
    p.lineTo(180.0,30.0);
    p.closePath();
 
    BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    drawPoints(g, p.getPathIterator(new AffineTransform()));
    g.dispose();
    
    ImageIO.write(image, "png", new File("donut_polygon_4.png"));
  }
  
  private static void drawPoints(Graphics2D g, PathIterator pi) {
 
    // 描画色
    Color[] c = {Color.RED, Color.GREEN, Color.BLUE};
    int cindex = 0;
 
    while (!pi.isDone()) {
      float coords[] = new float[6];
      int type = pi.currentSegment(coords);
      switch (type) {
      case PathIterator.SEG_MOVETO:
        g.setPaint(c[cindex]);
        g.drawString("M", coords[0], coords[1]);
        break;
      case PathIterator.SEG_LINETO:
        g.setPaint(c[cindex]);
        g.drawString("L", coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE:
        cindex++;
        break;
      }
      pi.next();
    }
  }
}

検証4: 別のドーナツポリゴンで点の起点を考慮して描画してみる

次はこんな感じのドーナツポリゴンで試してみる。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

一番上(最小のy値)の中で一番右(最大のx値)を起点にして描画してみる。でも、見事に失敗。

Java でドーナツポリゴン (穴あきポリゴン, Donut Polygon, Polygon with Holes) を描画する

ソースコード:


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
 
public class DonutPolygon {
 
  public static void main(String[] args) throws Exception {
    
    GeneralPath p = new GeneralPath(GeneralPath.WIND_NON_ZERO);
    // 内側
    p.moveTo(120, 60);
    p.lineTo(150, 90);
    p.lineTo(120, 120);
    p.lineTo(90, 120);
    p.lineTo(60, 90);
    p.lineTo(90, 60);
    p.closePath();
    // 外側
    p.moveTo(120, 30);
    p.lineTo(180, 60);
    p.lineTo(180, 180);
    p.lineTo(60, 180);
    p.lineTo(30, 60);
    p.lineTo(120, 30); // 外側の最初と同じ点を追加してみる
    p.closePath();
 
    BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    g.setPaint(Color.BLUE);
    g.fill(p);
    g.setStroke(new BasicStroke(5.0f));
    g.setPaint(Color.RED);
    g.draw(p);
    g.dispose();
    
    ImageIO.write(image, "png", new File("donut_polygon_5.png"));
  }
  
}

まぁ、 GeneralPath でドーナツポリゴンを描画する方法は公式ドキュメントにも見つからなかったので、素直に java.awt.geom.Area クラスを使うべきだと思う。。。

それと、 Java 1.6 からは GeneralPath じゃなくて Path2D, Path2D.Double, Path2D.Float を使ったほうがいいかも。同じようなものだけれどJava公式的な方針としては Path2D を使うのが主流になりそう。

tags: java

Posted by NI-Lab. (@nilab)