1 /*
   2  * Copyright (c) 1998, 2008, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text.html;
  26 
  27 import java.awt.Polygon;
  28 import java.io.Serializable;
  29 import java.util.StringTokenizer;
  30 import java.util.Vector;
  31 import javax.swing.text.AttributeSet;
  32 
  33 /**
  34  * Map is used to represent a map element that is part of an HTML document.
  35  * Once a Map has been created, and any number of areas have been added,
  36  * you can test if a point falls inside the map via the contains method.
  37  *
  38  * @author  Scott Violet
  39  */
  40 class Map implements Serializable {
  41     /** Name of the Map. */
  42     private String           name;
  43     /** An array of AttributeSets. */
  44     private Vector<AttributeSet>           areaAttributes;
  45     /** An array of RegionContainments, will slowly grow to match the
  46      * length of areaAttributes as needed. */
  47     private Vector<RegionContainment>           areas;
  48 
  49     public Map() {
  50     }
  51 
  52     public Map(String name) {
  53         this.name = name;
  54     }
  55 
  56     /**
  57      * Returns the name of the Map.
  58      */
  59     public String getName() {
  60         return name;
  61     }
  62 
  63     /**
  64      * Defines a region of the Map, based on the passed in AttributeSet.
  65      */
  66     public void addArea(AttributeSet as) {
  67         if (as == null) {
  68             return;
  69         }
  70         if (areaAttributes == null) {
  71             areaAttributes = new Vector<AttributeSet>(2);
  72         }
  73         areaAttributes.addElement(as.copyAttributes());
  74     }
  75 
  76     /**
  77      * Removes the previously created area.
  78      */
  79     public void removeArea(AttributeSet as) {
  80         if (as != null && areaAttributes != null) {
  81             int numAreas = (areas != null) ? areas.size() : 0;
  82             for (int counter = areaAttributes.size() - 1; counter >= 0;
  83                  counter--) {
  84                 if (areaAttributes.elementAt(counter).isEqual(as)){
  85                     areaAttributes.removeElementAt(counter);
  86                     if (counter < numAreas) {
  87                         areas.removeElementAt(counter);
  88                     }
  89                 }
  90             }
  91         }
  92     }
  93 
  94     /**
  95      * Returns the AttributeSets representing the differet areas of the Map.
  96      */
  97     public AttributeSet[] getAreas() {
  98         int numAttributes = (areaAttributes != null) ? areaAttributes.size() :
  99                             0;
 100         if (numAttributes != 0) {
 101             AttributeSet[]    retValue = new AttributeSet[numAttributes];
 102 
 103             areaAttributes.copyInto(retValue);
 104             return retValue;
 105         }
 106         return null;
 107     }
 108 
 109     /**
 110      * Returns the AttributeSet that contains the passed in location,
 111      * <code>x</code>, <code>y</code>. <code>width</code>, <code>height</code>
 112      * gives the size of the region the map is defined over. If a matching
 113      * area is found, the AttribueSet for it is returned.
 114      */
 115     public AttributeSet getArea(int x, int y, int width, int height) {
 116         int      numAttributes = (areaAttributes != null) ?
 117                                  areaAttributes.size() : 0;
 118 
 119         if (numAttributes > 0) {
 120             int      numAreas = (areas != null) ? areas.size() : 0;
 121 
 122             if (areas == null) {
 123                 areas = new Vector<RegionContainment>(numAttributes);
 124             }
 125             for (int counter = 0; counter < numAttributes; counter++) {
 126                 if (counter >= numAreas) {
 127                     areas.addElement(createRegionContainment
 128                             (areaAttributes.elementAt(counter)));
 129                 }
 130                 RegionContainment rc = areas.elementAt(counter);
 131                 if (rc != null && rc.contains(x, y, width, height)) {
 132                     return areaAttributes.elementAt(counter);
 133                 }
 134             }
 135         }
 136         return null;
 137     }
 138 
 139     /**
 140      * Creates and returns an instance of RegionContainment that can be
 141      * used to test if a particular point lies inside a region.
 142      */
 143     protected RegionContainment createRegionContainment
 144                                   (AttributeSet attributes) {
 145         Object     shape = attributes.getAttribute(HTML.Attribute.SHAPE);
 146 
 147         if (shape == null) {
 148             shape = "rect";
 149         }
 150         if (shape instanceof String) {
 151             String                shapeString = ((String)shape).toLowerCase();
 152             RegionContainment     rc = null;
 153 
 154             try {
 155                 if (shapeString.equals("rect")) {
 156                     rc = new RectangleRegionContainment(attributes);
 157                 }
 158                 else if (shapeString.equals("circle")) {
 159                     rc = new CircleRegionContainment(attributes);
 160                 }
 161                 else if (shapeString.equals("poly")) {
 162                     rc = new PolygonRegionContainment(attributes);
 163                 }
 164                 else if (shapeString.equals("default")) {
 165                     rc = DefaultRegionContainment.sharedInstance();
 166                 }
 167             } catch (RuntimeException re) {
 168                 // Something wrong with attributes.
 169                 rc = null;
 170             }
 171             return rc;
 172         }
 173         return null;
 174     }
 175 
 176     /**
 177      * Creates and returns an array of integers from the String
 178      * <code>stringCoords</code>. If one of the values represents a
 179      * % the returned value with be negative. If a parse error results
 180      * from trying to parse one of the numbers null is returned.
 181      */
 182     static protected int[] extractCoords(Object stringCoords) {
 183         if (stringCoords == null || !(stringCoords instanceof String)) {
 184             return null;
 185         }
 186 
 187         StringTokenizer    st = new StringTokenizer((String)stringCoords,
 188                                                     ", \t\n\r");
 189         int[]              retValue = null;
 190         int                numCoords = 0;
 191 
 192         while(st.hasMoreElements()) {
 193             String         token = st.nextToken();
 194             int            scale;
 195 
 196             if (token.endsWith("%")) {
 197                 scale = -1;
 198                 token = token.substring(0, token.length() - 1);
 199             }
 200             else {
 201                 scale = 1;
 202             }
 203             try {
 204                 int       intValue = Integer.parseInt(token);
 205 
 206                 if (retValue == null) {
 207                     retValue = new int[4];
 208                 }
 209                 else if(numCoords == retValue.length) {
 210                     int[]    temp = new int[retValue.length * 2];
 211 
 212                     System.arraycopy(retValue, 0, temp, 0, retValue.length);
 213                     retValue = temp;
 214                 }
 215                 retValue[numCoords++] = intValue * scale;
 216             } catch (NumberFormatException nfe) {
 217                 return null;
 218             }
 219         }
 220         if (numCoords > 0 && numCoords != retValue.length) {
 221             int[]    temp = new int[numCoords];
 222 
 223             System.arraycopy(retValue, 0, temp, 0, numCoords);
 224             retValue = temp;
 225         }
 226         return retValue;
 227     }
 228 
 229 
 230     /**
 231      * Defines the interface used for to check if a point is inside a
 232      * region.
 233      */
 234     interface RegionContainment {
 235         /**
 236          * Returns true if the location <code>x</code>, <code>y</code>
 237          * falls inside the region defined in the receiver.
 238          * <code>width</code>, <code>height</code> is the size of
 239          * the enclosing region.
 240          */
 241         public boolean contains(int x, int y, int width, int height);
 242     }
 243 
 244 
 245     /**
 246      * Used to test for containment in a rectangular region.
 247      */
 248     static class RectangleRegionContainment implements RegionContainment {
 249         /** Will be non-null if one of the values is a percent, and any value
 250          * that is non null indicates it is a percent
 251          * (order is x, y, width, height). */
 252         float[]       percents;
 253         /** Last value of width passed in. */
 254         int           lastWidth;
 255         /** Last value of height passed in. */
 256         int           lastHeight;
 257         /** Top left. */
 258         int           x0;
 259         int           y0;
 260         /** Bottom right. */
 261         int           x1;
 262         int           y1;
 263 
 264         public RectangleRegionContainment(AttributeSet as) {
 265             int[]    coords = Map.extractCoords(as.getAttribute(HTML.
 266                                                            Attribute.COORDS));
 267 
 268             percents = null;
 269             if (coords == null || coords.length != 4) {
 270                 throw new RuntimeException("Unable to parse rectangular area");
 271             }
 272             else {
 273                 x0 = coords[0];
 274                 y0 = coords[1];
 275                 x1 = coords[2];
 276                 y1 = coords[3];
 277                 if (x0 < 0 || y0 < 0 || x1 < 0 || y1 < 0) {
 278                     percents = new float[4];
 279                     lastWidth = lastHeight = -1;
 280                     for (int counter = 0; counter < 4; counter++) {
 281                         if (coords[counter] < 0) {
 282                             percents[counter] = Math.abs
 283                                         (coords[counter]) / 100.0f;
 284                         }
 285                         else {
 286                             percents[counter] = -1.0f;
 287                         }
 288                     }
 289                 }
 290             }
 291         }
 292 
 293         public boolean contains(int x, int y, int width, int height) {
 294             if (percents == null) {
 295                 return contains(x, y);
 296             }
 297             if (lastWidth != width || lastHeight != height) {
 298                 lastWidth = width;
 299                 lastHeight = height;
 300                 if (percents[0] != -1.0f) {
 301                     x0 = (int)(percents[0] * width);
 302                 }
 303                 if (percents[1] != -1.0f) {
 304                     y0 = (int)(percents[1] * height);
 305                 }
 306                 if (percents[2] != -1.0f) {
 307                     x1 = (int)(percents[2] * width);
 308                 }
 309                 if (percents[3] != -1.0f) {
 310                     y1 = (int)(percents[3] * height);
 311                 }
 312             }
 313             return contains(x, y);
 314         }
 315 
 316         public boolean contains(int x, int y) {
 317             return ((x >= x0 && x <= x1) &&
 318                     (y >= y0 && y <= y1));
 319         }
 320     }
 321 
 322 
 323     /**
 324      * Used to test for containment in a polygon region.
 325      */
 326     static class PolygonRegionContainment extends Polygon implements
 327                  RegionContainment {
 328         /** If any value is a percent there will be an entry here for the
 329          * percent value. Use percentIndex to find out the index for it. */
 330         float[]           percentValues;
 331         int[]             percentIndexs;
 332         /** Last value of width passed in. */
 333         int               lastWidth;
 334         /** Last value of height passed in. */
 335         int               lastHeight;
 336 
 337         public PolygonRegionContainment(AttributeSet as) {
 338             int[]    coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
 339                                                                 COORDS));
 340 
 341             if (coords == null || coords.length == 0 ||
 342                 coords.length % 2 != 0) {
 343                 throw new RuntimeException("Unable to parse polygon area");
 344             }
 345             else {
 346                 int        numPercents = 0;
 347 
 348                 lastWidth = lastHeight = -1;
 349                 for (int counter = coords.length - 1; counter >= 0;
 350                      counter--) {
 351                     if (coords[counter] < 0) {
 352                         numPercents++;
 353                     }
 354                 }
 355 
 356                 if (numPercents > 0) {
 357                     percentIndexs = new int[numPercents];
 358                     percentValues = new float[numPercents];
 359                     for (int counter = coords.length - 1, pCounter = 0;
 360                          counter >= 0; counter--) {
 361                         if (coords[counter] < 0) {
 362                             percentValues[pCounter] = coords[counter] /
 363                                                       -100.0f;
 364                             percentIndexs[pCounter] = counter;
 365                             pCounter++;
 366                         }
 367                     }
 368                 }
 369                 else {
 370                     percentIndexs = null;
 371                     percentValues = null;
 372                 }
 373                 npoints = coords.length / 2;
 374                 xpoints = new int[npoints];
 375                 ypoints = new int[npoints];
 376 
 377                 for (int counter = 0; counter < npoints; counter++) {
 378                     xpoints[counter] = coords[counter + counter];
 379                     ypoints[counter] = coords[counter + counter + 1];
 380                 }
 381             }
 382         }
 383 
 384         public boolean contains(int x, int y, int width, int height) {
 385             if (percentValues == null || (lastWidth == width &&
 386                                           lastHeight == height)) {
 387                 return contains(x, y);
 388             }
 389             // Force the bounding box to be recalced.
 390             bounds = null;
 391             lastWidth = width;
 392             lastHeight = height;
 393             float fWidth = (float)width;
 394             float fHeight = (float)height;
 395             for (int counter = percentValues.length - 1; counter >= 0;
 396                  counter--) {
 397                 if (percentIndexs[counter] % 2 == 0) {
 398                     // x
 399                     xpoints[percentIndexs[counter] / 2] =
 400                             (int)(percentValues[counter] * fWidth);
 401                 }
 402                 else {
 403                     // y
 404                     ypoints[percentIndexs[counter] / 2] =
 405                             (int)(percentValues[counter] * fHeight);
 406                 }
 407             }
 408             return contains(x, y);
 409         }
 410     }
 411 
 412 
 413     /**
 414      * Used to test for containment in a circular region.
 415      */
 416     static class CircleRegionContainment implements RegionContainment {
 417         /** X origin of the circle. */
 418         int           x;
 419         /** Y origin of the circle. */
 420         int           y;
 421         /** Radius of the circle. */
 422         int           radiusSquared;
 423         /** Non-null indicates one of the values represents a percent. */
 424         float[]       percentValues;
 425         /** Last value of width passed in. */
 426         int           lastWidth;
 427         /** Last value of height passed in. */
 428         int           lastHeight;
 429 
 430         public CircleRegionContainment(AttributeSet as) {
 431             int[]    coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
 432                                                                 COORDS));
 433 
 434             if (coords == null || coords.length != 3) {
 435                 throw new RuntimeException("Unable to parse circular area");
 436             }
 437             x = coords[0];
 438             y = coords[1];
 439             radiusSquared = coords[2] * coords[2];
 440             if (coords[0] < 0 || coords[1] < 0 || coords[2] < 0) {
 441                 lastWidth = lastHeight = -1;
 442                 percentValues = new float[3];
 443                 for (int counter = 0; counter < 3; counter++) {
 444                     if (coords[counter] < 0) {
 445                         percentValues[counter] = coords[counter] /
 446                                                  -100.0f;
 447                     }
 448                     else {
 449                         percentValues[counter] = -1.0f;
 450                     }
 451                 }
 452             }
 453             else {
 454                 percentValues = null;
 455             }
 456         }
 457 
 458         public boolean contains(int x, int y, int width, int height) {
 459             if (percentValues != null && (lastWidth != width ||
 460                                           lastHeight != height)) {
 461                 int      newRad = Math.min(width, height) / 2;
 462 
 463                 lastWidth = width;
 464                 lastHeight = height;
 465                 if (percentValues[0] != -1.0f) {
 466                     this.x = (int)(percentValues[0] * width);
 467                 }
 468                 if (percentValues[1] != -1.0f) {
 469                     this.y = (int)(percentValues[1] * height);
 470                 }
 471                 if (percentValues[2] != -1.0f) {
 472                     radiusSquared = (int)(percentValues[2] *
 473                                    Math.min(width, height));
 474                     radiusSquared *= radiusSquared;
 475                 }
 476             }
 477             return (((x - this.x) * (x - this.x) +
 478                      (y - this.y) * (y - this.y)) <= radiusSquared);
 479         }
 480     }
 481 
 482 
 483     /**
 484      * An implementation that will return true if the x, y location is
 485      * inside a rectangle defined by origin 0, 0, and width equal to
 486      * width passed in, and height equal to height passed in.
 487      */
 488     static class DefaultRegionContainment implements RegionContainment {
 489         /** A global shared instance. */
 490         static DefaultRegionContainment  si = null;
 491 
 492         public static DefaultRegionContainment sharedInstance() {
 493             if (si == null) {
 494                 si = new DefaultRegionContainment();
 495             }
 496             return si;
 497         }
 498 
 499         public boolean contains(int x, int y, int width, int height) {
 500             return (x <= width && x >= 0 && y >= 0 && y <= width);
 501         }
 502     }
 503 }