001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.webSvr.location;
031    
032    import java.awt.Dimension;
033    import java.awt.image.BufferedImage;
034    import java.io.IOException;
035    import java.lang.ref.SoftReference;
036    
037    import javax.servlet.ServletContext;
038    import javax.servlet.http.HttpServletRequest;
039    
040    import org.hd.d.pg2k.svrCore.ImageUtils;
041    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
042    import org.hd.d.pg2k.svrCore.location.Location;
043    import org.hd.d.pg2k.webSvr.util.WebUtils;
044    
045    import ORG.hd.d.IsDebug;
046    
047    /**
048     * Created by IntelliJ IDEA.
049     * User: Damon Hart-Davis
050     * Date: 01-Sep-2003
051     * Time: 18:40:11
052     */
053    
054    /**Utilities to support Location display in Web pages.
055     *
056     */
057    public class LocationUtils
058        {
059        /**Width of (raw and modified) Estd thumbnail in pixels as returned by getEstdLocationThumbnailURL(). */
060        public static final int LOC_TN_WIDTH = 32;
061    
062        /**Height of (raw and modified) Estd thumbnail in pixels as returned by getEstdLocationThumbnailURL(). */
063        public static final int LOC_TN_HEIGHT = 22;
064    
065        /**Type of thumbnail image that we dynamically generate.
066         * Need not be the same as the raw thumbnail image,
067         * as long as it can (efficiently) encode the modified thumbnail
068         * in a byte-indexed format.
069         * <p>
070         * Good types to use are PNG and GIF (when the LZW patent expires).
071         */
072        static final int DYN_LOC_TN_IMG_TYPE = ExhibitMIME.ET_PNG;
073    
074        /**ExhibitMIME entry for the chosen output type. */
075        static final ExhibitMIME.ExhibitTypeParameters ETP =
076            ExhibitMIME.getParamsByType(DYN_LOC_TN_IMG_TYPE);
077    
078        /**Colour of cross-hairs to show location of item in RGB format.
079         * Since the icon is usually rendered in green (land) and blue (sea)
080         * this should probably be red or some other high-contrast colour.
081         * This should probably also be fully opaque to minimise encoded image size
082         * (eg 0xffff0000 for opaque red).
083         * <p>
084         * Make this available to (for example) Aloha Earth,
085         * so that we can use a common colour/style throughout.
086         */
087        public static final int DYN_LOC_TN_CROSSHAIR_RGB_COLOUR = 0xffff0000; // Opaque red.
088    
089        /**Basic root-relative URL for base thumbnail icon.
090         * This has to be an image type that we can decode, modify, and encode
091         * on the fly.
092         * <p>
093         * Assumed to be low-colour suitable for an index/palette representation
094         * such as GIF or PNG.
095         * <p>
096         * By default rendered n green (land) and blue (sea).
097         * <p>
098         * ON OE is assumed to be centred in the image,
099         * nominally between pixels for an even-pixel dimension.
100         */
101        static final String BASE_ESTD_LOC_TN_URL = "/_static/icon/Earth.png";
102    
103        /**Mount point for servlet, including usual suffix for given image type.
104         * May mount this in the "/_static/" area to help clients cache the
105         * modified thumbnails and thus reducing redundant requests for them.
106         * <p>
107         * This must correspond with where the servlet is actually mounted.
108         * (The servlet can be mounted to serve a tree that includes this.)
109         */
110        private static final String DYN_LOC_TN_MOUNT_POINT =
111            "/_static/locEstdTN/Earth" + ETP.dotSuffixForInputFile;
112    
113    
114        /**Get cross-hair X,Y pixel centre on thumbnail given Estd location.
115         * Since the dimension values are integral,
116         * this rounds to the nearest correct pixel value.
117         * <p>
118         * The result is constrained to valid pixel coordinates
119         * [0, width[ and [0, height[.
120         */
121        private static Dimension getTNCrosshairCentre(final Location.Estd loc)
122            {
123            if(loc == null) { throw new IllegalArgumentException(); }
124    
125            final float ePos = loc.getE().value.floatValue() / Location.Estd.MAX_E;
126            final float nPos = loc.getN().value.floatValue() / Location.Estd.MAX_N;
127    
128            final int x = Math.max(0, Math.min(LOC_TN_WIDTH - 1,
129                                   Math.round(((1 + ePos) / 2.0f) * LOC_TN_WIDTH)));
130            // Note that y axis polarity is reverse of N dimension.
131            final int y = Math.max(0, Math.min(LOC_TN_HEIGHT - 1,
132                                   Math.round(((1 - nPos) / 2.0f) * LOC_TN_HEIGHT)));
133    
134            return(new Dimension(x, y));
135            }
136    
137        /**Return root-relative (starting with "/") URL for Estd location thumbnail.
138         * May be fixed, or may depend on actual location.
139         *
140         * @param loc the location on Earth to link to the map for; never null
141         * @param embedInHTML  if true, result will be embedded in HTML
142         *     so we will separate parameters with '&amp;amp;' rather than
143         *     an unprotected '&amp;'
144         */
145        public static String getEstdLocationThumbnailRRL(final Location.Estd loc,
146                                                         final boolean embedInHTML)
147            {
148            final Dimension dim = getTNCrosshairCentre(loc);
149            final StringBuilder result =
150                new StringBuilder(DYN_LOC_TN_MOUNT_POINT.length() + 16);
151            result.append(DYN_LOC_TN_MOUNT_POINT);
152            result.append("?x=").append(dim.width);
153            if(embedInHTML) { result.append("&amp;"); } else { result.append('&'); }
154            result.append("y=").append(dim.height);
155            return(result.toString());
156            }
157    
158        /**Returns a modified location thumbnail with crosshairs centred on x,y; never null.
159         * This will create an image on first use,
160         * and cache it (via a SoftReference).
161         * <p>
162         * The byte[] returned is not copied and so must not be altered.
163         * This routine is only package-visible.
164         * <p>
165         * This routine is synchronized primarily to make the cache thread-safe,
166         * but has the side-effect of ensuring that only one thumbnail
167         * can be generated at once, thus limiting peak CPU and memory demands.
168         *
169         * @param context  context of current WAR; must not be null
170         * @param x  x coordinate of crosshair centre within thumbnail
171         * @param y  x coordinate of crosshair centre within thumbnail
172         * @return encoded image for modified thumbnail;
173         *     this is a shared copy and must not be altered by the caller
174         *     which is one reason that the routine is only package-visible
175         * @throws java.lang.IllegalArgumentException  if context is null
176         * @throws java.lang.RuntimeException  if x or y not valid pixel coords
177         *     within the thumbnail, ie where the crosshair could be centred.
178         * @throws java.io.IOException  if difficulty generating modified thumbnail
179         *     for example because the site is too busy
180         */
181        static synchronized byte[] getLocTNWithCrosshairs(
182                                                    final ServletContext context,
183                                                    final int x, final int y)
184            throws IllegalArgumentException,
185            RuntimeException,
186            IOException
187            {
188            byte result[];
189            if((_gLTWC_cache[x][y] == null) ||
190                ((result = _gLTWC_cache[x][y].get()) == null))
191                {
192                // Refuse to actually generate new thumbnail if overloaded.
193                if(WebUtils.isOverloaded(context))
194                    { throw new IOException("too busy to generate thumbnail"); }
195    
196                final BufferedImage in =
197                    WebUtils.getAndCacheStaticImage(true, // Get a copy.
198                                                    BASE_ESTD_LOC_TN_URL,
199                                                    true, // Force to true colour.
200                                                    context); // Let this be GCed.
201    
202                // Draw in the crosshairs...
203                // Do not overwrite at focus so that target pixel can be seen.
204                for(int yc = LOC_TN_HEIGHT; --yc >= 0; )
205                    { if(yc != y) { in.setRGB(x, yc, DYN_LOC_TN_CROSSHAIR_RGB_COLOUR); } }
206                for(int xc = LOC_TN_WIDTH; --xc >= 0; )
207                    { if(xc != x) { in.setRGB(xc, y, DYN_LOC_TN_CROSSHAIR_RGB_COLOUR); } }
208    
209                // Force to indexed image; four colours should be enough/efficient,
210                // and may allow packing of at least 4 pixels to the byte.
211                final BufferedImage out = ImageUtils.makeColourReducedBufferedImage(
212                    in, 4, true);
213    
214                // Generate and cache encoded image.
215                // Don't attempt to fudge the quality.
216                result = ETP.handler.makeImageBinary(out, Integer.MAX_VALUE);
217                if(result == null) { throw new IOException(); }
218                _gLTWC_cache[x][y] = new SoftReference<byte[]>(result);
219                }
220            return(result);
221            }
222    
223        /**Private cache for getLocTNWithCrosshairs() accessed under the class lock.
224         * Arranged as (x,y)-crosshair-centre array of
225         * SoftReference to byte[] encoded images.
226         */
227        @SuppressWarnings("unchecked")
228        private static final SoftReference<byte[]> _gLTWC_cache[][] =
229           new SoftReference[LOC_TN_WIDTH][LOC_TN_HEIGHT];
230    
231    
232        /**Make HTML img for correct Estd location thumbnail with E/N crosshairs; may be empty but never null.
233         * If there is no location then return the empty string.
234         * <p>
235         * The returned img has the alignment "top" and alt text "Aloha Earth!"
236         * and no whitespace around it.
237         */
238        public static String makeLocTNImgHTML(final HttpServletRequest request, final Location.Estd location)
239            {
240            if(location == null) { return(""); }
241    
242            final int capacity = 145;
243            final StringBuilder result = new StringBuilder(capacity);
244            result.append("<img src=\"").
245                append(WebUtils.getOptionalSneakyConcurrencyRRURLPrefix(request)).
246                append(LocationUtils.getEstdLocationThumbnailRRL(location, true)).
247                append('"');
248            result.append(" width=").append(LOC_TN_WIDTH);
249            result.append(" height=").append(LOC_TN_HEIGHT);
250            result.append(" align=top alt=\"Aloha Earth!\" title=\"Aloha Earth!\">");
251    if(IsDebug.isDebug && ((result.length() > capacity) || (result.length() < capacity/2))) { System.err.println("WARNING: makeLocTNImgHTML(): len="+result.length()+" vs capacity="+capacity+"."); }
252            return(result.toString());
253            }
254        }