001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.html.link;
018
019import java.awt.Shape;
020import java.awt.geom.PathIterator;
021
022import org.apache.wicket.AttributeModifier;
023import org.apache.wicket.Component;
024import org.apache.wicket.behavior.Behavior;
025import org.apache.wicket.markup.ComponentTag;
026import org.apache.wicket.markup.html.image.Image;
027import org.apache.wicket.markup.html.panel.Panel;
028import org.apache.wicket.markup.repeater.RepeatingView;
029import org.apache.wicket.model.Model;
030import org.apache.wicket.model.PropertyModel;
031
032/**
033 * A client-side image map implementation which allows you to "attach" the map to any existing
034 * {@link Image} component.
035 * 
036 * @since 1.5
037 */
038public class ClientSideImageMap extends Panel
039{
040        private static final long serialVersionUID = 1L;
041        private static final String CIRCLE = "circle";
042        private static final String POLYGON = "poly";
043        private static final String RECTANGLE = "rect";
044
045        private RepeatingView areas;
046
047        /**
048         * Constructs a client-side image map which is "attached" to the given {@link Image} component.
049         * 
050         * @param id
051         *            the component id
052         * @param image
053         *            the image component
054         */
055        public ClientSideImageMap(String id, Image image)
056        {
057                super(id);
058                setOutputMarkupId(true);
059                add(AttributeModifier.replace("name", new PropertyModel<String>(this, "markupId")));
060                image.add(AttributeModifier.replace("usemap", new UsemapModel()));
061
062                areas = new RepeatingView("area");
063                add(areas);
064        }
065
066        private String circleCoordinates(int x, int y, int radius)
067        {
068                return x + "," + y + "," + radius;
069        }
070
071        private String polygonCoordinates(int... coordinates)
072        {
073                final StringBuilder buffer = new StringBuilder();
074                for (int i = 0; i < coordinates.length; i++)
075                {
076                        buffer.append(coordinates[i]);
077
078                        if (i < (coordinates.length - 1))
079                        {
080                                buffer.append(',');
081                        }
082                }
083                return buffer.toString();
084        }
085
086        private String rectangleCoordinates(int x1, int y1, int x2, int y2)
087        {
088                return x1 + "," + y1 + "," + x2 + "," + y2;
089        }
090
091        private String shapeCoordinates(Shape shape)
092        {
093                final StringBuilder sb = new StringBuilder();
094                final PathIterator pi = shape.getPathIterator(null, 1.0);
095                final float[] coords = new float[6];
096                final float[] lastMove = new float[2];
097                while (!pi.isDone())
098                {
099                        switch (pi.currentSegment(coords))
100                        {
101                                case PathIterator.SEG_MOVETO :
102                                        if (sb.length() != 0)
103                                        {
104                                                sb.append(',');
105                                        }
106                                        sb.append(Math.round(coords[0]));
107                                        sb.append(',');
108                                        sb.append(Math.round(coords[1]));
109                                        lastMove[0] = coords[0];
110                                        lastMove[1] = coords[1];
111                                        break;
112                                case PathIterator.SEG_LINETO :
113                                        if (sb.length() != 0)
114                                        {
115                                                sb.append(',');
116                                        }
117                                        sb.append(Math.round(coords[0]));
118                                        sb.append(',');
119                                        sb.append(Math.round(coords[1]));
120                                        break;
121                                case PathIterator.SEG_CLOSE :
122                                        if (sb.length() != 0)
123                                        {
124                                                sb.append(',');
125                                        }
126                                        sb.append(Math.round(lastMove[0]));
127                                        sb.append(',');
128                                        sb.append(Math.round(lastMove[1]));
129                                        break;
130                        }
131                        pi.next();
132                }
133                return sb.toString();
134        }
135
136        @Override
137        protected void onComponentTag(ComponentTag tag)
138        {
139                // Must be attached to an map tag
140                checkComponentTag(tag, "map");
141
142                super.onComponentTag(tag);
143        }
144
145        /**
146         * Generates a unique id string. This makes it easy to add items to be rendered w/out having to
147         * worry about generating unique id strings in your code.
148         * 
149         * @return unique child id
150         */
151        public String newChildId()
152        {
153                return areas.newChildId();
154        }
155
156        /**
157         * Adds a circle-shaped area centered at (x,y) with radius r.
158         * 
159         * @param link
160         *            the link
161         * @param x
162         *            x coordinate of the center of the circle
163         * @param y
164         *            y coordinate of center
165         * @param radius
166         *            the radius
167         * @return this
168         */
169        public ClientSideImageMap addCircleArea(AbstractLink link, int x, int y, int radius)
170        {
171                areas.add(link);
172
173                link.add(new Area(circleCoordinates(x, y, radius), CIRCLE));
174
175                return this;
176        }
177
178        /**
179         * Adds a polygon-shaped area defined by coordinates.
180         * 
181         * @param link
182         *            the link
183         * @param coordinates
184         *            the coordinates for the polygon
185         * @return This
186         */
187        public ClientSideImageMap addPolygonArea(AbstractLink link, int... coordinates)
188        {
189                areas.add(link);
190
191                link.add(new Area(polygonCoordinates(coordinates), POLYGON));
192
193                return this;
194        }
195
196        /**
197         * Adds a rectangular-shaped area.
198         * 
199         * @param link
200         *            the link
201         * @param x1
202         *            top left x
203         * @param y1
204         *            top left y
205         * @param x2
206         *            bottom right x
207         * @param y2
208         *            bottom right y
209         * @return this
210         */
211        public ClientSideImageMap addRectangleArea(AbstractLink link, int x1, int y1, int x2, int y2)
212        {
213                areas.add(link);
214                link.add(new Area(rectangleCoordinates(x1, y1, x2, y2), RECTANGLE));
215                return this;
216        }
217
218        /**
219         * Adds an area defined by a shape object.
220         * 
221         * @param link
222         *            the link
223         * @param shape
224         *            the shape
225         * @return this
226         */
227        public ClientSideImageMap addShapeArea(AbstractLink link, Shape shape)
228        {
229                areas.add(link);
230                link.add(new Area(shapeCoordinates(shape), POLYGON));
231                return this;
232        }
233
234        /**
235         * Encapsulates the concept of an <area> within a <map>.
236         */
237        private static class Area extends Behavior
238        {
239                private static final long serialVersionUID = 1L;
240
241                private final String coordinates;
242                private final String type;
243
244                protected Area(final String coordinates, final String type)
245                {
246                        this.coordinates = coordinates;
247                        this.type = type;
248                }
249
250                @Override
251                public void onComponentTag(Component component, ComponentTag tag)
252                {
253                        tag.put("shape", type);
254                        tag.put("coords", coordinates);
255                }
256        }
257
258        private class UsemapModel extends Model<String>
259        {
260                private static final long serialVersionUID = 1L;
261
262                @Override
263                public String getObject()
264                {
265                        return "#" + getMarkupId();
266                }
267        }
268}