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 }