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