Guillaume F.
Guillaume F.

Reputation: 1130

JavaFX WebView: how to add HyperLink listener to usemap / area?

While using WebView, there a way to add a pseudo HyperlinkListener for <img> tags making use of the usemap attribute to refer the click to a map?

For example, in the following HTML from w3schools,

<h1>The map and area elements</h1>

<p>Click on the sun or on one of the planets to watch it closer:</p>

<img src="planets.gif" width="145" height="126" alt="Planets" usemap="#planetmap">

<map name="planetmap">
  <area shape="rect" coords="0,0,82,126" alt="Sun" href="sun.htm">
  <area shape="circle" coords="90,58,3" alt="Mercury" href="mercur.htm">
  <area shape="circle" coords="124,58,8" alt="Venus" href="venus.htm">
</map>

is there a way to get an event listener for when the user clicks on the "Sun" element?

Related: solution for hyperlink listeners with <a> tags, HyperlinkListener in JavaFX WebEngine.

Upvotes: 2

Views: 178

Answers (1)

VGR
VGR

Reputation: 44385

I was able to do it with these steps:

  • Wait for web page to load.
  • Use XPath to locate the <img> element.
  • Use the usemap value of <img> to locate the corresponding <map> element.
  • Obtain a list of all <area> children of the <map>.
  • Add a click listener to the <map> element, which:
    • Uses a JavaScript getBoundingClientRect() call to retrieve the <img> element’s bounds relative to the viewport.
    • Obtains the mouse click coordinates relative to the viewport.
    • Iterates the <area> child elements, and for each one, parses the coords attribute into a list of values, then creates a Shape corresponding to the shape attribute.
    • Adjusts the mouse click coordinates so they’re relative to the origin of the <img>.
    • Checks whether those mouse click coordinates are inside the Shape.

Here’s a complete demo:

import java.net.URI;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.events.MouseEvent;

import javafx.application.Application;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;

import javafx.scene.shape.Shape;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Polygon;

import javafx.scene.web.WebView;
import javafx.scene.web.WebEngine;

import netscape.javascript.JSObject;

public class WebViewImageMapTest
extends Application {

    /**
     * A single coordinate in the list which comprises the value of
     * the {@code coords} attribute of an {@code <area>} element.
     */
    private static class Coordinate {
        final double value;
        final boolean percentage;

        Coordinate(double value,
                   boolean percentage) {
            this.value = value;
            this.percentage = percentage;
        }

        double resolveAgainst(double size) {
            return percentage ? value * size / 100 : value;
        }

        static Coordinate parse(String s) {
            if (s.endsWith("%")) {
                return new Coordinate(
                    Double.parseDouble(s.substring(0, s.length() - 1)), true);
            } else {
                return new Coordinate(Double.parseDouble(s), false);
            }
        }

        @Override
        public String toString() {
            return getClass().getName() +
                "[" + value + (percentage ? "%" : "") + "]";
        }
    }

    @Override
    public void start(Stage stage) {
        WebView view = new WebView();

        Label destination = new Label(" ");
        destination.setPadding(new Insets(12));

        WebEngine engine = view.getEngine();
        engine.getLoadWorker().stateProperty().addListener((o, old, state) -> {
            if (state != Worker.State.SUCCEEDED) {
                return;
            }

            Document doc = engine.getDocument();
            try {
                XPath xpath = XPathFactory.newInstance().newXPath();

                Element img = (Element)
                    xpath.evaluate("//*[local-name()='img']",
                        doc, XPathConstants.NODE);

                String mapURI = img.getAttribute("usemap");
                String mapID = URI.create(mapURI).getFragment();
                Element map = doc.getElementById(mapID);
                if (map == null) {
                    // No <map> with matching id.
                    // Look for <map> with matching name instead.
                    map = (Element) xpath.evaluate(
                        "//*[local-name()='map'][@name='" + mapID + "']",
                        doc, XPathConstants.NODE);
                }

                NodeList areas = (NodeList)
                    xpath.evaluate("//*[local-name()='area']",
                        map, XPathConstants.NODESET);

                ((EventTarget) map).addEventListener("click", e -> {
                    Element area = getClickedArea(
                        (MouseEvent) e, areas, getClientBounds(img, engine));

                    if (area != null) {
                        destination.setText(area.getAttribute("href"));
                    } else {
                        destination.setText(" ");
                    }
                }, false);
            } catch (XPathException e) {
                e.printStackTrace();
            }
        });

        engine.load(
            WebViewImageMapTest.class.getResource("imgmap.html").toString());

        stage.setScene(new Scene(
            new BorderPane(view, null, null, destination, null)));
        stage.setTitle("Image Map Test");
        stage.show();
    }

    /**
     * Returns the bounds of an element relative to the viewport.
     */
    private Rectangle getClientBounds(Element element,
                                      WebEngine engine) {

        JSObject window = (JSObject) engine.executeScript("window");
        window.setMember("desiredBoundsElement", element);
        JSObject bounds = (JSObject) engine.executeScript(
            "desiredBoundsElement.getBoundingClientRect();");

        Number n;
        n = (Number) bounds.getMember("x");
        double x = n.doubleValue();
        n = (Number) bounds.getMember("y");
        double y = n.doubleValue();
        n = (Number) bounds.getMember("width");
        double width = n.doubleValue();
        n = (Number) bounds.getMember("height");
        double height = n.doubleValue();

        return new Rectangle(x, y, width, height);
    }

    private Element getClickedArea(MouseEvent event,
                                   NodeList areas,
                                   Rectangle imgClientBounds) {

        int clickX = event.getClientX();
        int clickY = event.getClientY();

        double imgX = imgClientBounds.getX();
        double imgY = imgClientBounds.getY();
        double imgWidth = imgClientBounds.getWidth();
        double imgHeight = imgClientBounds.getHeight();

        int count = areas.getLength();
        for (int i = 0; i < count; i++) {
            Element area = (Element) areas.item(i);

            String shapeType = area.getAttribute("shape");
            if (shapeType == null) {
                shapeType = "";
            }

            String[] rawCoords = area.getAttribute("coords").split(",");
            int numCoords = rawCoords.length;
            Coordinate[] coords = new Coordinate[numCoords];
            for (int c = 0; c < numCoords; c++) {
                coords[c] = Coordinate.parse(rawCoords[c].trim());
            }

            Shape shape = null;
            switch (shapeType) {
                case "rect":
                    double left = coords[0].resolveAgainst(imgWidth);
                    double top = coords[1].resolveAgainst(imgHeight);
                    double right = coords[2].resolveAgainst(imgWidth);
                    double bottom = coords[3].resolveAgainst(imgHeight);
                    shape = new Rectangle(
                        left, top, right - left, bottom - top);
                    break;
                case "circle":
                    double centerX = coords[0].resolveAgainst(imgWidth);
                    double centerY = coords[1].resolveAgainst(imgHeight);
                    double radius = coords[2].resolveAgainst(
                        Math.min(imgWidth, imgHeight));
                    shape = new Circle(centerX, centerY, radius);
                    break;
                case "poly":
                    double[] polygonCoords = new double[coords.length];
                    for (int c = polygonCoords.length - 1; c >= 0; c--) {
                        polygonCoords[c] = coords[c].resolveAgainst(
                            c % 2 == 0 ? imgWidth : imgHeight);
                    }
                    shape = new Polygon(polygonCoords);
                    break;
                default:
                    shape = new Rectangle(imgWidth, imgHeight);
                    break;
            }

            if (shape.contains(clickX - imgX, clickY - imgY)) {
                return area;
            }
        }

        return null;
    }

    public static class Main {
        public static void main(String[] args) {
            Application.launch(WebViewImageMapTest.class, args);
        }
    }
}

And here is the planets.gif I used:

enter image description here

Upvotes: 1

Related Questions