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 
Map()50     public Map() {
51     }
52 
Map(String name)53     public Map(String name) {
54         this.name = name;
55     }
56 
57     /**
58      * Returns the name of the Map.
59      */
getName()60     public String getName() {
61         return name;
62     }
63 
64     /**
65      * Defines a region of the Map, based on the passed in AttributeSet.
66      */
addArea(AttributeSet as)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      */
removeArea(AttributeSet as)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      */
getAreas()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      */
getArea(int x, int y, int width, int height)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      */
createRegionContainment(AttributeSet attributes)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      */
extractCoords(Object stringCoords)183     protected static 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          */
contains(int x, int y, int width, int height)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 
RectangleRegionContainment(AttributeSet as)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 
contains(int x, int y, int width, int height)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 
contains(int x, int y)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 
PolygonRegionContainment(AttributeSet as)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 
contains(int x, int y, int width, int height)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 
CircleRegionContainment(AttributeSet as)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 
contains(int x, int y, int width, int height)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 
sharedInstance()493         public static DefaultRegionContainment sharedInstance() {
494             if (si == null) {
495                 si = new DefaultRegionContainment();
496             }
497             return si;
498         }
499 
contains(int x, int y, int width, int height)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 }
505