View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.CSS_DISPLAY_BLOCK;
18  
19  import java.io.IOException;
20  import java.util.Arrays;
21  import java.util.Map;
22  
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.htmlunit.SgmlPage;
26  import org.htmlunit.javascript.host.event.Event;
27  import org.htmlunit.util.StringUtils;
28  import org.htmlunit.util.geometry.Circle2D;
29  import org.htmlunit.util.geometry.Polygon2D;
30  import org.htmlunit.util.geometry.Rectangle2D;
31  import org.htmlunit.util.geometry.Shape2D;
32  
33  /**
34   * Wrapper for the HTML element "area".
35   *
36   * @author Mike Bowler
37   * @author David K. Taylor
38   * @author Christian Sell
39   * @author Marc Guillemot
40   * @author Ahmed Ashour
41   * @author Frank Danek
42   * @author Ronald Brill
43   */
44  public class HtmlArea extends HtmlElement implements HyperlinkElement {
45      private static final Log LOG = LogFactory.getLog(HtmlArea.class);
46  
47      /** The HTML tag represented by this element. */
48      public static final String TAG_NAME = "area";
49  
50      private static final String SHAPE_RECT = "rect";
51      private static final String SHAPE_CIRCLE = "circle";
52      private static final String SHAPE_POLY = "poly";
53  
54      /**
55       * Creates a new instance.
56       *
57       * @param qualifiedName the qualified name of the element type to instantiate
58       * @param page the page that contains this element
59       * @param attributes the initial attributes
60       */
61      HtmlArea(final String qualifiedName, final SgmlPage page,
62              final Map<String, DomAttr> attributes) {
63          super(qualifiedName, page, attributes);
64      }
65  
66      /**
67       * {@inheritDoc}
68       */
69      @Override
70      protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
71          HyperlinkElementSupport.doClickStateUpdate(this, shiftKey, ctrlKey, "");
72          return false;
73      }
74  
75      /**
76       * Returns the value of the attribute {@code shape}. Refer to the
77       * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
78       * documentation for details on the use of this attribute.
79       *
80       * @return the value of the attribute {@code shape} or an empty string if that attribute isn't defined
81       */
82      public final String getShapeAttribute() {
83          return getAttributeDirect("shape");
84      }
85  
86      /**
87       * Returns the value of the attribute {@code coords}. Refer to the
88       * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
89       * documentation for details on the use of this attribute.
90       *
91       * @return the value of the attribute {@code coords} or an empty string if that attribute isn't defined
92       */
93      public final String getCoordsAttribute() {
94          return getAttributeDirect("coords");
95      }
96  
97      /**
98       * Returns the value of the attribute {@code href}. Refer to the
99       * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
100      * documentation for details on the use of this attribute.
101      *
102      * @return the value of the attribute {@code href} or an empty string if that attribute isn't defined
103      */
104     @Override
105     public final String getHrefAttribute() {
106         return getAttributeDirect("href");
107     }
108 
109     /**
110      * Returns the value of the attribute {@code nohref}. Refer to the
111      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
112      * documentation for details on the use of this attribute.
113      *
114      * @return the value of the attribute {@code nohref} or an empty string if that attribute isn't defined
115      */
116     public final String getNoHrefAttribute() {
117         return getAttributeDirect("nohref");
118     }
119 
120     /**
121      * Returns the value of the attribute {@code alt}. Refer to the
122      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
123      * documentation for details on the use of this attribute.
124      *
125      * @return the value of the attribute {@code alt} or an empty string if that attribute isn't defined
126      */
127     public final String getAltAttribute() {
128         return getAttributeDirect("alt");
129     }
130 
131     /**
132      * Returns the value of the attribute {@code tabindex}. Refer to the
133      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
134      * documentation for details on the use of this attribute.
135      *
136      * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
137      */
138     public final String getTabIndexAttribute() {
139         return getAttributeDirect("tabindex");
140     }
141 
142     /**
143      * Returns the value of the attribute {@code accesskey}. Refer to the
144      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
145      * documentation for details on the use of this attribute.
146      *
147      * @return the value of the attribute {@code accesskey} or an empty string if that attribute isn't defined
148      */
149     public final String getAccessKeyAttribute() {
150         return getAttributeDirect("accesskey");
151     }
152 
153     /**
154      * Returns the value of the attribute {@code onfocus}. Refer to the
155      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
156      * documentation for details on the use of this attribute.
157      *
158      * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
159      */
160     public final String getOnFocusAttribute() {
161         return getAttributeDirect("onfocus");
162     }
163 
164     /**
165      * Returns the value of the attribute {@code onblur}. Refer to the
166      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
167      * documentation for details on the use of this attribute.
168      *
169      * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
170      */
171     public final String getOnBlurAttribute() {
172         return getAttributeDirect("onblur");
173     }
174 
175     /**
176      * Returns the value of the attribute {@code target}. Refer to the
177      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
178      * documentation for details on the use of this attribute.
179      *
180      * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
181      */
182     @Override
183     public final String getTargetAttribute() {
184         return getAttributeDirect("target");
185     }
186 
187     /**
188      * Returns the value of the attribute {@code rel}. Refer to the
189      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
190      * documentation for details on the use of this attribute.
191      *
192      * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
193      */
194     @Override
195     public final String getRelAttribute() {
196         return getAttributeDirect("rel");
197     }
198 
199     /**
200      * Returns the value of the attribute {@code ping} or an empty string if that attribute isn't defined.
201      *
202      * @return the value of the attribute {@code ping} or an empty string if that attribute isn't defined
203      */
204     @Override
205     public final String getPingAttribute() {
206         return getAttributeDirect("ping");
207     }
208 
209     /**
210      * Returns the value of the attribute {@code download} or an empty string if that attribute isn't defined.
211      *
212      * @return the value of the attribute {@code download} or an empty string if that attribute isn't defined
213      */
214     @Override
215     public final String getDownloadAttribute() {
216         return getAttributeDirect("download");
217     }
218 
219     /**
220      * Indicates if this area contains the specified point.
221      * @param x the x coordinate of the point
222      * @param y the y coordinate of the point
223      * @return {@code true} if the point is contained in this area
224      */
225     boolean containsPoint(final int x, final int y) {
226         final String shape = StringUtils.defaultIfEmptyOrNull(getShapeAttribute(), SHAPE_RECT);
227 
228         if ("default".equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
229             return true;
230         }
231 
232         if (SHAPE_RECT.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
233             final Shape2D rectangle = parseRect();
234             return rectangle.contains(x, y);
235         }
236 
237         if (SHAPE_CIRCLE.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
238             final Shape2D circle = parseCircle();
239             return circle.contains(x, y);
240         }
241 
242         if (SHAPE_POLY.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
243             final Shape2D path = parsePoly();
244             return path.contains(x, y);
245         }
246 
247         return false;
248     }
249 
250     /**
251      * {@inheritDoc}
252      */
253     @Override
254     public DisplayStyle getDefaultStyleDisplay() {
255         if (hasFeature(CSS_DISPLAY_BLOCK)) {
256             return DisplayStyle.NONE;
257         }
258         return DisplayStyle.INLINE;
259     }
260 
261     /**
262      * {@inheritDoc}
263      */
264     @Override
265     public boolean isDisplayed() {
266         final DomNode parent = getParentNode();
267         if (parent instanceof HtmlMap && parent.isDisplayed()) {
268             return !isEmpty();
269         }
270         return false;
271     }
272 
273     private Rectangle2D parseRect() {
274         // browsers seem to support comma and blank
275         final String[] coords = StringUtils.splitAtCommaOrBlank(getCoordsAttribute());
276 
277         double leftX = 0;
278         double topY = 0;
279         double rightX = 0;
280         double bottomY = 0;
281 
282         try {
283             if (coords.length > 0) {
284                 leftX = Double.parseDouble(coords[0].trim());
285             }
286             if (coords.length > 1) {
287                 topY = Double.parseDouble(coords[1].trim());
288             }
289             if (coords.length > 2) {
290                 rightX = Double.parseDouble(coords[2].trim());
291             }
292             if (coords.length > 3) {
293                 bottomY = Double.parseDouble(coords[3].trim());
294             }
295         }
296         catch (final NumberFormatException e) {
297             if (LOG.isWarnEnabled()) {
298                 LOG.warn("Invalid rect coords '" + Arrays.toString(coords) + "'", e);
299             }
300         }
301 
302         return new Rectangle2D(leftX, topY, rightX, bottomY);
303     }
304 
305     private Circle2D parseCircle() {
306         // browsers seem to support comma and blank
307         final String[] coords = StringUtils.splitAtCommaOrBlank(getCoordsAttribute());
308 
309         double centerX = 0;
310         double centerY = 0;
311         double radius = 0;
312 
313         try {
314             if (coords.length > 0) {
315                 centerX = Double.parseDouble(coords[0].trim());
316             }
317             if (coords.length > 1) {
318                 centerY = Double.parseDouble(coords[1].trim());
319             }
320             if (coords.length > 2) {
321                 radius = Double.parseDouble(coords[2].trim());
322             }
323 
324         }
325         catch (final NumberFormatException e) {
326             if (LOG.isWarnEnabled()) {
327                 LOG.warn("Invalid circle coords '" + Arrays.toString(coords) + "'", e);
328             }
329         }
330 
331         return new Circle2D(centerX, centerY, radius);
332     }
333 
334     private Shape2D parsePoly() {
335         // browsers seem to support comma and blank
336         final String[] coords = StringUtils.splitAtCommaOrBlank(getCoordsAttribute());
337 
338         try {
339             if (coords.length > 1) {
340                 double x = Double.parseDouble(coords[0].trim());
341                 double y = Double.parseDouble(coords[1].trim());
342 
343                 final Polygon2D path = new Polygon2D(x, y);
344 
345                 for (int i = 2; i + 1 < coords.length; i += 2) {
346                     x = Double.parseDouble(coords[i].trim());
347                     y = Double.parseDouble(coords[i + 1].trim());
348 
349                     path.lineTo(x, y);
350                 }
351                 return path;
352             }
353         }
354         catch (final NumberFormatException e) {
355             if (LOG.isWarnEnabled()) {
356                 LOG.warn("Invalid poly coords '" + Arrays.toString(coords) + "'", e);
357             }
358         }
359 
360         return new Rectangle2D(0, 0, 0, 0);
361     }
362 
363     /**
364      * {@inheritDoc}
365      */
366     @Override
367     public boolean handles(final Event event) {
368         if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
369             return true;
370         }
371         return super.handles(event);
372     }
373 
374     private boolean isEmpty() {
375         final String shape = StringUtils.defaultIfEmptyOrNull(getShapeAttribute(), SHAPE_RECT);
376 
377         if ("default".equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
378             return false;
379         }
380 
381         if (SHAPE_RECT.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
382             final Shape2D rectangle = parseRect();
383             return rectangle.isEmpty();
384         }
385 
386         if (SHAPE_CIRCLE.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
387             final Shape2D circle = parseCircle();
388             return circle.isEmpty();
389         }
390 
391         if (SHAPE_POLY.equalsIgnoreCase(shape) && getCoordsAttribute() != null) {
392             final Shape2D path = parsePoly();
393             return path.isEmpty();
394         }
395 
396         return false;
397     }
398 }