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