001    /*
002    Copyright (c) 1996-2011, 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.virtualHosts.AlohaEarth;
031    
032    import java.awt.Point;
033    import java.awt.Rectangle;
034    import java.io.Serializable;
035    import java.util.Properties;
036    import java.util.StringTokenizer;
037    
038    import javax.servlet.http.HttpServletRequest;
039    
040    import org.hd.d.pg2k.svrCore.PGException;
041    import org.hd.d.pg2k.svrCore.location.Location;
042    import org.hd.d.pg2k.svrCore.uploader.UploaderUtils;
043    
044    /**JavaBean used to parse/validate/adjust/save view parameters.
045     * This can have the zoom and (x,y)-offset parameters of a particular
046     * view set in various ways, and will coerce them to valid values.
047     * This can also be used to generate suitably-encoded parameters for
048     * a visitor to navigate in the Aloha Earth space.
049     * <p>
050     * So that this is a valid JavaBean we make it Serializable,
051     * but in practice never expect it to be persisted,
052     * even temporarily in a session,
053     * so we don't bother with a unique serialisation ID, validation, etc,
054     * but we do make any derived internal values transient.
055     * <p>
056     * This is <strong>NOT</strong> currently designed to be thread-safe.
057     * <p>
058     * This implements Clonable to as to make it possible to easily
059     * take a copy of the parameters, alter them, and turn the new values
060     * into a form's parameter set, for example.
061     * <p>
062     * The main parameters held in this bean are:
063     * <ul>
064     * <li>The zoom factor: the amount the original image is zoomed in/out
065     *     for display to the user.  Bigger numbers mean more zoom in,
066     *     ie greater expansion.
067     * <li>The offset of the center of the (zoomed, shifted) image shown to the user
068     *     north and east from the original image (which is centered on 0N OE).
069     * </ul>
070     * These parameters are constrained internally to legal range,
071     * independently of one another.  On extraction of the values with
072     * accessor methods the values may optionally be contrained to mutually-legal
073     * combinations.
074     * <p>
075     * Note that for the East Offset and North Offset,
076     * the offset is measured from the centre of the current image
077     * to the centre of the original underlying image, where either
078     * centre may potentially lie within a pixel or between pixels.
079     * <p>
080     * The unit of measure is pixels of the original image,
081     * making the units invariant wrt the zoom factor of the current image,
082     * and thus this value is constrained to lie in the range
083     * [-BASE_2D_EARTH_MAP_WIDTH/2, +BASE_2D_EARTH_MAP_WIDTH/2] for EO, and
084     * [-BASE_2D_EARTH_MAP_HEIGHT/2, +BASE_2D_EARTH_MAP_HEIGHT/2] for NO
085     * though usually within a narrower range than that.
086     * <p>
087     * Note that the fact that when we zoom in one pixel in the original
088     * takes several pixels in the current/displayed image means that
089     * careful handling is required to get image alignment correct.
090     */
091    public final class AEParams implements Serializable, Cloneable
092        {
093        /**If true, round offset values to "tile" multiples.
094         * This ensures that fewer unique views can be seen,
095         * which should make cacheing feasable.
096         * <p>
097         * Across each view in each direction (E and N)
098         * are ZOOM_RATIO tiles.
099         * A user may move laterally in either dimension one tile width,
100         * and may zoom in to one of the tiles of the current image.
101         * Plainly, tiles from adjacent "windows" can in principle be shared,
102         * which we might make use of explicitly with a tiled display in HTML,
103         * or implicitly by say having JAI draw the image on demand and
104         * have a Renderable in cache.
105         */
106        public static final boolean FORCE_TILE_POSITIONING = true;
107    
108        /**Field name for map image as mouse-sensitive submit button.
109         * If the user clicks on the map image, we should receive a
110         * pair of [name].x and [name].y parameters of where the user clicked.
111         */
112        public static final String NAME_MAP_FIELD = "map";
113    
114        /**Amount to add to zoomFactor on clicking on the map image; 0 means no zoom. */
115        public static final int MAP_CLICK_ZF_ADJ = +1;
116    
117        /**Name of zoom-control field (submit button).
118         * This, if set, carries changes to be applied after the
119         * zoom factor has been set from the NAME_ZOOM_FACTOR parameter.
120         */
121        public static final String NAME_ZOOM_CONTROL_FIELD = "zoomCtl";
122    
123        /**External name of zoomFactor parameter, eg in HTTP GET/POST. */
124        public static final String NAME_ZOOM_FACTOR = "zf";
125    
126        /**The current zoom factor.
127         * Constrained to lie between
128         * MIN_ZOOM and MAX_ZOOM inclusive,
129         * and INITIAL_ZOOM unless otherwise set (ie the initial value).
130         */
131        private int zoomFactor = AEUtils.INITIAL_ZOOM;
132    
133        /**The absolute maximum limit (+/-) of the East Offset in pixels; positive and rounded down. */
134        public static final int OFFSET_LIMIT_EAST = AEUtils.BASE_2D_EARTH_MAP_WIDTH/2;
135    
136        /**The absolute maximum limit (+/-) of the NORTH Offset in pixels; positive and rounded down. */
137        public static final int OFFSET_LIMIT_NORTH = AEUtils.BASE_2D_EARTH_MAP_HEIGHT/2;
138    
139        /**Get the current zoom factor.
140         * Constrained to lie between
141         * MIN_ZOOM and MAX_ZOOM inclusive,
142         * and INITIAL_ZOOM unless otherwise set.
143         */
144        public int getZoomFactor() { return(zoomFactor); }
145    
146        /**Set the current zoom factor as an integer; will be coerced to range.
147         * Constrained to lie between
148         * MIN_ZOOM and MAX_ZOOM inclusive,
149         * and the value will be coerced to those limits.
150         *
151         * @return reference to this modified object
152         */
153        public AEParams setZoomFactor(int zf)
154            {
155            if(zf < AEUtils.MIN_ZOOM)
156                { zf = AEUtils.MIN_ZOOM; }
157            else if(zf > AEUtils.MAX_ZOOM)
158                { zf = AEUtils.MAX_ZOOM; }
159            zoomFactor = zf;
160            return(this);
161            }
162    
163        /**Set the current value as a String; parse and coerced as necessary.
164         * Parsed as a decimal (possibly-signed) integer
165         * and constrained to lie between
166         * MIN_ZOOM and MAX_ZOOM inclusive,
167         * and the value will be coerced to those limits.
168         * <p>
169         * In case of a parse error (including a null argument)
170         * no value is set.
171         *
172         * @return reference to this modified object
173         */
174        public AEParams setZoomFactor(final String zf)
175            {
176            try { setZoomFactor(Integer.parseInt(zf, 10)); }
177            // Simply ignore set attempt in case of null or bad argument.
178            catch(final NumberFormatException e) { }
179            return(this);
180            }
181    
182        /**External name of North Offset parameter, eg in HTTP GET/POST. */
183        public static final String NAME_NORTH_OFFSET = "no";
184    
185        /**External name of East Offset parameter, eg in HTTP GET/POST. */
186        public static final String NAME_EAST_OFFSET = "eo";
187    
188        /**The East Offset of of the current view from original image.
189         * The offset is measured from the centre of the current image
190         * to the centre of the original underlying image, where either
191         * centre may potentially lie within a pixel or between pixels.
192         * <p>
193         * This value is constrained to lie in the range
194         * [-BASE_2D_EARTH_MAP_WIDTH/2, +BASE_2D_EARTH_MAP_WIDTH/2].
195         * <p>
196         * The initial value is zero.
197         */
198        private int eastOffset; // Initially zero.
199    
200        /**The North Offset of of the current view from original image.
201         * As the centre of the user's view goes more north,
202         * this becomes more positive.
203         * <p>
204         * The offset of measured from the centre of the current image
205         * to the centre of the original underlying image, where either
206         * centre may potentially lie within a pixel or between pixels.
207         * <p>
208         * This value is constrained to lie in the range
209         * [-BASE_2D_EARTH_MAP_HEIGHT/2, +BASE_2D_EARTH_MAP_HEIGHT/2].
210         * <p>
211         * Note that is is the negative of the change in the y axis
212         * which in computer graphics usually has higher values lower down.
213         * <p>
214         * The initial value is zero.
215         */
216        private int northOffset; // Initially zero.
217    
218    
219        /**Set the East Offset of the current view from the base/original image.
220         * As the centre of the user's view goes more east,
221         * this becomes more positive.
222         * <p>
223         * The offset of measured from the centre of the current image
224         * to the centre of the original underlying image, where either
225         * centre may potentially lie within a pixel or between pixels.
226         * <p>
227         * This value is constrained to lie in the range
228         * [-BASE_2D_EARTH_MAP_WIDTH/2, +BASE_2D_EARTH_MAP_WIDTH/2];
229         * any value outside these bounds is coerced to range.
230         * <p>
231         * When read back "constrained" this is coerced to try to keep
232         * the current view within the original source image.
233         * <p>
234         * The initial (unset/default) value is zero.
235         * <p>
236         *
237         * @return reference to this modified object
238         */
239        public AEParams setEastOffset(final int pixelsOffset)
240            {
241            // Coerce into range if need be.
242            if(pixelsOffset < -OFFSET_LIMIT_EAST)
243                { eastOffset = -OFFSET_LIMIT_EAST; }
244            else if (pixelsOffset > OFFSET_LIMIT_EAST)
245                { eastOffset = OFFSET_LIMIT_EAST; }
246            else
247                { eastOffset = pixelsOffset; }
248    
249            return(this);
250            }
251    
252        /**Set East offset as String.
253         * Unparseable (or null) values are ignored,
254         * parseable values are coerced to fit the valid range.
255         *
256         * @return reference to this modified object
257         */
258        public AEParams setEastOffset(final String pixelsOffset)
259            {
260            try { setEastOffset(Integer.parseInt(pixelsOffset, 10)); }
261            // Simply ignore set attempt in case of null or bad argument.
262            catch(final NumberFormatException e) { }
263            return(this);
264            }
265    
266        /**Set the North Offset of the current view from the base/original image.
267         * As the centre of the user's view goes more north,
268         * this becomes more positive.
269         * <p>
270         * The offset of measured from the centre of the current image
271         * to the centre of the original underlying image, where either
272         * centre may potentially lie within a pixel or between pixels.
273         * <p>
274         * This value is constrained to lie in the range
275         * [-BASE_2D_EARTH_MAP_HEIGHT/2, +BASE_2D_EARTH_MAP_HEIGHT/2];
276         * any value outside these bounds is coerced to range.
277         * <p>
278         * When read back "constrained" this is coerced to try to keep
279         * the current view within the original source image.
280         * <p>
281         * The initial (unset/default) value is zero.
282         * <p>
283         *
284         * @return reference to this modified object
285         */
286        public AEParams setNorthOffset(final int pixelsOffset)
287            {
288            // Coerce into range if need be.
289            if(pixelsOffset < -OFFSET_LIMIT_NORTH)
290                { northOffset = -OFFSET_LIMIT_NORTH; }
291            else if (pixelsOffset > OFFSET_LIMIT_NORTH)
292                { northOffset = OFFSET_LIMIT_NORTH; }
293            else
294                { northOffset = pixelsOffset; }
295    
296            return(this);
297            }
298    
299        /**Set North offset as String.
300         * Unparseable (or null) values are ignored,
301         * parseable values are coerced to fit the valid range.
302         *
303         * @return reference to this modified object
304         */
305        public AEParams setNorthOffset(final String pixelsOffset)
306            {
307            try { setNorthOffset(Integer.parseInt(pixelsOffset, 10)); }
308            // Simply ignore set attempt in case of null or bad argument.
309            catch(final NumberFormatException e) { }
310            return(this);
311            }
312    
313    
314    
315        /**Get East Offset of the current view from the base/original image.
316         * As the centre of the user's view goes more east,
317         * this becomes more positive.
318         * <p>
319         * The offset of measured from the centre of the current image
320         * to the centre of the original underlying image, where either
321         * centre may potentially lie within a pixel or between pixels.
322         * <p>
323         * This value is constrained to lie in the range
324         * [-BASE_2D_EARTH_MAP_WIDTH/2, +BASE_2D_EARTH_MAP_WIDTH/2]
325         * at most.
326         * <p>
327         * If called for a "constrained" value, the return is clamped
328         * so as to ensure that all of the current view lies within
329         * the base image if possible.  In all cases the result, if adjusted,
330         * is adjusted towards zero.
331         * <p>
332         * If we are rounding to tile widths, we round down.
333         * <p>
334         * The initial value is zero.
335         *
336         * @param constrained  if true, return is if necessary adjusted towards
337         *     zero to try to force the current zoomed view to be from entirely
338         *     within the source image and possibly rounded to a tile width,
339         *     else the raw (but still clamped) value is returned
340         */
341        public int getEastOffset(final boolean constrained)
342            {
343            if(!constrained) { return(eastOffset); }
344    
345            // Shift the apparent centre to keep the current viewport within
346            // the base image.
347            int eo = eastOffset;
348    
349            // Zero is a special case and is never adjusted.
350            if(eo == 0)
351                { return(0); }
352    
353            // Compute a limit on the offset which keep the rectangle of pixels
354            // extracted from the source image within the source image.
355            final int offsetLimit = OFFSET_LIMIT_EAST -
356                ((getBaseSrcPixelsWidth(true) + 1) / 2); // Rounded-up measure.
357            if(eo < -offsetLimit) { eo = -offsetLimit; }
358            else if(eo > offsetLimit) { eo = offsetLimit; }
359    
360            // Round inwards to tile boundary towards (0,0) if needed.
361            if(FORCE_TILE_POSITIONING)
362                {
363                final int tw = getTileSrcPixelsWidth();
364                eo = (eo / tw) * tw;
365                }
366    
367            // Return possibly-adjusted value.
368            return(eo);
369            }
370    
371        /**Get source image left (x) coordinate.
372         * As the centre of the user's view goes more east,
373         * this becomes more positive.
374         * <p>
375         * This calls getEastOffset().
376         *
377         * @param constrained  if true, the result is always within
378         *     the source image bounds [0,BASE_2D_EARTH_MAP_WIDTH-1] inclusive
379         *     and if FORCE_TILE_POSITIONING is true will be rounded to a tile size
380         */
381        private int getEastOffsetSrcPixelsX(final boolean constrained)
382            {
383            final int width = getBaseSrcPixelsWidth(constrained);
384            final int x = ((AEUtils.BASE_2D_EARTH_MAP_WIDTH - width + 1) / 2)
385                + getEastOffset(constrained);
386    //System.out.println("getEastOffsetSrcPixelsX(): constrained="+constrained+", width="+width+", x="+x+", eo="+getEastOffset(false));
387            if(constrained)
388                {
389                if(x < 0)
390                    { return(0); }
391                assert(width > 0);
392                if(x + width > AEUtils.BASE_2D_EARTH_MAP_WIDTH)
393                    { return(Math.max(0, AEUtils.BASE_2D_EARTH_MAP_WIDTH - width)); }
394                }
395            return(x);
396            }
397    
398        /**Get North Offset of the current view from original image.
399         * As the centre of the user's view goes more north,
400         * this becomes more positive.
401         * <p>
402         * The offset of measured from the centre of the current image
403         * to the centre of the original underlying image, where either
404         * centre may potentially lie within a pixel or between pixels.
405         * <p>
406         * This value is constrained to lie in the range
407         * [-BASE_2D_EARTH_MAP_HEIGHT/2, +BASE_2D_EARTH_MAP_HEIGHT/2]
408         * at most.
409         * <p>
410         * Note that is is the negative of the change in the y axis
411         * which in computer graphics usually has higher values lower down.
412         * <p>
413         * The initial value is zero.
414         *
415         * @param constrained  if true, return is if necessary adjusted towards
416         *     zero to try to force the current zoomed view to be from entirely
417         *     within the source image,
418         *     else the raw (but still clamped) value is returned
419         */
420        public int getNorthOffset(final boolean constrained)
421            {
422            if(!constrained) { return(northOffset); }
423    
424            // Shift the apparent centre to keep the current viewport within
425            // the base image.
426            int no = northOffset;
427    
428            // Zero is a special case and is never adjusted.
429            if(no == 0)
430                { return(0); }
431    
432            // Compute a limit on the offset which keep the rectangle of pixels
433            // extracted from the source image within the source image.
434            final int offsetLimit = OFFSET_LIMIT_NORTH -
435                ((getBaseSrcPixelsHeight(true) + 1) / 2); // Rounded-up measure.
436            if(no < -offsetLimit) { no = -offsetLimit; }
437            else if(no > offsetLimit) { no = offsetLimit; }
438    
439            // Round inwards to tile boundary towards (0,0) if needed.
440            if(FORCE_TILE_POSITIONING)
441                {
442                final int th = getTileSrcPixelsHeight();
443                no = (no / th) * th;
444                }
445    
446            // Return possibly-adjusted value.
447            return(no);
448            }
449    
450        /**Get source image top (y) coordinate.
451         * As the centre of the user's view goes more north,
452         * this becomes more <strong>negative</strong>.
453         * <p>
454         * This calls getNorthOffset().
455         *
456         * @param constrained  if true, the result is always within
457         *     the source image bounds [0,BASE_2D_EARTH_MAP_HEIGHT-1] inclusive
458         *     and if FORCE_TILE_POSITIONING is true will be rounded to a tile size
459         */
460        private int getNorthOffsetSrcPixelsY(final boolean constrained)
461            {
462            final int height = getBaseSrcPixelsHeight(constrained);
463            final int y = ((AEUtils.BASE_2D_EARTH_MAP_HEIGHT - height + 1) / 2)
464                - getNorthOffset(constrained);
465    //System.out.println("getNorthOffsetSrcPixelsY(): constrained="+constrained+", height="+height+", y="+y+", no="+getNorthOffset(false));
466            if(constrained)
467                {
468                if(y < 0)
469                    { return(0); }
470                assert(height > 0);
471                if(y + height > AEUtils.BASE_2D_EARTH_MAP_HEIGHT)
472                    { return(Math.max(0, AEUtils.BASE_2D_EARTH_MAP_HEIGHT - height)); }
473                }
474            return(y);
475            }
476    
477        /**Get East Offset as degrees East; range approximately [-180f, +180f].
478         * As the centre of the user's view goes more east,
479         * this becomes more positive.
480         * <p>
481         * The conversion will be a bit lumpy and inexact,
482         * especially as the underlying step in the offset is likely to be similar
483         * but not identical to a degree.  The caller could usefully round this.
484         * <p>
485         * Clamped to +/- Location.Estd.MAX_E.
486         *
487         * @param constrained  when true, if the source image area would lie
488         *     partly or completely outside the base image, this is adjusted
489         *     in line with getEastOffset() towards zero to try
490         *     to bring it within the base image
491         */
492        public float getEastDegrees(final boolean constrained)
493            {
494            return(Math.min(Location.Estd.MAX_E, Math.max(-Location.Estd.MAX_E,
495                (getEastOffset(constrained) * 360.0f) / AEUtils.BASE_2D_EARTH_MAP_WIDTH)));
496            }
497    
498        /**Get North Offset as degrees North; range approximately [-90f, +90f].
499         * As the centre of the user's view goes more north,
500         * this becomes more positive.
501         * <p>
502         * The conversion will be a bit lumpy and inexact,
503         * especially as the underlying step in the offset is likely to be similar
504         * but not identical to a degree.  The caller could usefully round this.
505         * <p>
506         * Clamped to +/- Location.Estd.MAX_N.
507         */
508        public float getNorthDegrees(final boolean constrained)
509            {
510            return(Math.min(Location.Estd.MAX_N, Math.max(-Location.Estd.MAX_N,
511                (getNorthOffset(constrained) * 180.0f) / AEUtils.BASE_2D_EARTH_MAP_HEIGHT)));
512            }
513    
514        /**Get current offset as a Location.Estd; never null.
515         * The error bounds indicate the area covered by the current view.
516         */
517        public Location.Estd getLocation(final boolean constrained)
518            {
519            // Compute the Error bounds as half the number of degrees
520            // implied by the width and height of the constained display
521            // source relative to the full source image.
522            final float NErr =
523                (90.0f * getBaseSrcPixelsHeight(constrained)) / AEUtils.BASE_2D_EARTH_MAP_HEIGHT;
524            final float EErr =
525                (180.0f * getBaseSrcPixelsWidth(constrained)) / AEUtils.BASE_2D_EARTH_MAP_WIDTH;
526    
527            // TODO: eliminate wasteful conversion to/from String if possible.
528            final Properties props = new Properties();
529            props.setProperty("type", "Estd");
530            props.setProperty("E", String.valueOf(getEastDegrees(constrained)));
531            props.setProperty("EErr", String.valueOf(EErr));
532            props.setProperty("N", String.valueOf(getNorthDegrees(constrained)));
533            props.setProperty("NErr", String.valueOf(NErr));
534            // Construct a simple example Estd item.
535            final Location.Estd e1;
536            try {
537                e1 = new Location.Estd(
538                            true, // Claim that this data is "specific" to a particular exhibit.
539                            "", // No prefix on the property names.
540                            props // The properties.
541                            );
542                }
543            catch(final PGException e) // Should not happen.
544                { throw new Error("internal error" + e.getMessage()); }
545            return(e1);
546            }
547    
548        /**Gets pixel in current view corresponding to centre of given location; null if not in current view.
549         *
550         * @param targetLoc  location to find pixel for centre of; null if not visible
551         */
552        public Point getDisplayPixelForEstdLocationCentre(final Location.Estd targetLoc)
553            {
554            // If not even in the current view, return null.
555            final Location.Estd viewArea = getLocation(true);
556            if(!viewArea.containsCentre(targetLoc))
557                { return(null); }
558    
559            final float targetN = targetLoc.getN().value.floatValue();
560            final int y = computeYfromN(targetN);
561    
562            final float targetE = targetLoc.getE().value.floatValue();
563            final int x = computeXfromE(targetE);
564    
565            return(new Point(x, y));
566            }
567    
568        /**Compute X pixel from E degrees given current view; may not be visible. */
569        int computeXfromE(final float targetE)
570            {
571            final Location.Estd viewArea = getLocation(true);
572            final float offE = targetE - viewArea.getE().value.floatValue();
573            final int maxWidth = AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH-1;
574            final int x = Math.max(0, Math.min(maxWidth,
575                Math.round((maxWidth+1) * 0.5f * ((offE / viewArea.getE().error.floatValue()) + 1))));
576            return x;
577            }
578    
579        /**Compute Y pixel from N degrees given current view; may not be visible. */
580        int computeYfromN(final float targetN)
581            {
582            final Location.Estd viewArea = getLocation(true);
583            final float offN = targetN - viewArea.getN().value.floatValue();
584            final int maxHeight = AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT-1;
585            final int y = maxHeight - Math.max(0, Math.min(maxHeight,
586                Math.round((maxHeight+1) * 0.5f *((offN / viewArea.getN().error.floatValue()) + 1))));
587            return y;
588            }
589    
590        /**Get bounding rectangle of area to be zoomed to make display image.
591         * Specified in pixels of the original image.
592         * <p>
593         * Depends on zoom and offsets.
594         * <p>
595         * Clamped so that no part of this will lie outside the original image,
596         * but this may change the aspect ratio, for example.
597         * <p>
598         * The width and height are always strictly positive.
599         * <p>
600         * Note that we adjust the zoom we use so that the minimum allowable
601         * zoom shows the whole image, but shunk or expanded from the raw image
602         * if the MIN_ZOOM is non-zero.
603         */
604        public Rectangle getSourceRectangleToDisplay()
605            {
606            final int clX = getEastOffsetSrcPixelsX(true);
607            final int clY = getNorthOffsetSrcPixelsY(true);
608            final int clWidth = getBaseSrcPixelsWidth(true);
609            final int clHeight = getBaseSrcPixelsHeight(true);
610            return(new Rectangle(clX, clY, clWidth, clHeight));
611            }
612    
613        /**Get height of current zoomed view in terms of original base-image pixels; always positive.
614         * Depends only on the zoom factor and does not (mutually) recurse with
615         * any of the the other accessor functions.
616         *
617         * @param constrained  if true, the result is constrained to be no more
618         *     than the height of the base image
619         */
620        public int getBaseSrcPixelsHeight(final boolean constrained)
621            {
622            final int nzf = _negativeZoomFactor();
623            final int height = Math.max(1,
624                AEUtils.applyZoom(AEUtils.BASE_2D_EARTH_MAP_HEIGHT, nzf));
625            if(constrained)
626                {
627                if(height > AEUtils.BASE_2D_EARTH_MAP_HEIGHT)
628                    { return(AEUtils.BASE_2D_EARTH_MAP_HEIGHT); }
629                }
630            return(height);
631            }
632    
633        /**Get width of current zoomed view in terms of original base-image pixels; always positive.
634         * Depends only on the zoom factor and does not (mutually) recurse with
635         * any of the the other accessor functions.
636         *
637         * @param constrained  if true, the result is constrained to be no more
638         *     than the width of the base image
639         */
640        public int getBaseSrcPixelsWidth(final boolean constrained)
641            {
642            final int nzf = _negativeZoomFactor();
643            final int width = Math.max(1,
644                AEUtils.applyZoom(AEUtils.BASE_2D_EARTH_MAP_WIDTH, nzf));
645            if(constrained)
646                {
647                if(width > AEUtils.BASE_2D_EARTH_MAP_WIDTH)
648                    { return(AEUtils.BASE_2D_EARTH_MAP_WIDTH); }
649                }
650            return(width);
651            }
652    
653        /**Get tile width in source-image pixels; strictly positive.
654         * Is the rounded-down current image width in source-image pixels
655         * divided by ZOOM_FACTOR, ie the number of steps left or right
656         * a user view is the same as the ratio by which we zoom in or out.
657         */
658        public int getTileSrcPixelsWidth()
659            {
660            return(getBaseSrcPixelsWidth(false) / AEUtils.ZOOM_RATIO);
661            }
662    
663        /**Get tile height in source-image pixels; strictly positive.
664         * Is the rounded-down current image height in source-image pixels
665         * divided by ZOOM_FACTOR, ie the number of steps up or down
666         * a user view is the same as the ratio by which we zoom in or out.
667         */
668        public int getTileSrcPixelsHeight()
669            {
670            return(getBaseSrcPixelsHeight(false) / AEUtils.ZOOM_RATIO);
671            }
672    
673    
674        /**Adjusts East/North offsets given x,y coordinates of mouse click on displayed area.
675         * The x is allowed to range from [0,DISPLAY_2D_EARTH_MAP_WIDTH[
676         * and y from [0,DISPLAY_2D_EARTH_MAP_HEIGHT[
677         * with the effect of moving the centre offset of the image
678         * to be as close as possible to the source image pixel
679         * representing the displayed map-fragment pixel clicked on.
680         * <p>
681         * This assumes that the displayed map fragment has a constrained center
682         * and width/height.
683         * <p>
684         * Coerces values to allowed range if need be.
685         */
686        public void adjustCentreWithXYClick(int x, int y)
687            {
688            if(x < 0) { x = 0; }
689            else if(x >= AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH)
690                { x = AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH - 1; }
691            if(y < 0) { y = 0; }
692            else if(y >= AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT)
693                { y = AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT - 1; }
694    
695            final Rectangle rect = getSourceRectangleToDisplay();
696    
697            setEastOffset(getEastOffset(true) + Math.round(rect.width *
698                ((((float) x) / (AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH - 1)) - .5f)));
699            setNorthOffset(getNorthOffset(true) - Math.round(rect.height *
700                ((((float) y) / (AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT - 1)) - .5f)));
701            }
702    
703        /**Computes a negated zoom factor rebased to the minimum zoom.
704         * This is used to compute the actual source image width and height
705         * from which the displayed image is extracted and zoomed to size.
706         *
707         * @return non-positive factor
708         */
709        private int _negativeZoomFactor()
710            {
711            return(-(getZoomFactor() - AEUtils.INITIAL_ZOOM));
712            }
713    
714        /**Prefix for (first) path component for embedded parameters, in the order prefix/e/n/z. */
715        private static final String PARAMS_PREFIX = "coords";
716    
717        /**If true then the PARAMS_PREFIX will not be visible in the path info. */
718        private static final boolean PARAMS_PREFIX_NOT_IN_PATHINFO = true;
719    
720        /**Sets all parameters possible from a servlet request.
721         * Arguments are parsed and coerced to correct values
722         * or ignored if missing or badly broken,
723         * leaving previous values in place.
724         * <p>
725         * Where necessary, arguments are coerced to form a consistent set.
726         * <p>
727         * This coercion may depend on data gathered from the current
728         * exhibit data set and base clickable-map image can cached statically
729         * where we can get at it.
730         * <p>
731         * This first sets the basic values from their simple parameter values,
732         * the applies any adjustment implied by such things as the zoom control
733         * and/or the map (x,y) click location.
734         *
735         * @return reference to this modified object
736         */
737        public AEParams setRequest(final HttpServletRequest request)
738            {
739            // Properties are tried after parsing components of the path,
740            // so properties win in case of conflict,
741            // and in particular to allow new params to override extant path.
742            final String pathInfo = request.getPathInfo();
743            if(pathInfo != null)
744                {
745    //System.out.println("AEParams.setRequest(): pathInfo="+pathInfo+", query="+request.getQueryString());
746                final StringTokenizer st = new StringTokenizer(pathInfo, "/");
747                if(PARAMS_PREFIX_NOT_IN_PATHINFO ||
748                   (st.hasMoreTokens() && PARAMS_PREFIX.equals(st.nextToken())))
749                    {
750                    if(st.hasMoreTokens()) { setEastOffset(st.nextToken()); }
751                    if(st.hasMoreTokens()) { setNorthOffset(st.nextToken()); }
752                    if(st.hasMoreTokens()) { setZoomFactor(st.nextToken()); }
753                    }
754                }
755            // Override with form params if present.
756            setEastOffset(request.getParameter(NAME_EAST_OFFSET));
757            setNorthOffset(request.getParameter(NAME_NORTH_OFFSET));
758            setZoomFactor(request.getParameter(NAME_ZOOM_FACTOR));
759    
760            // If there is a zoom-control parameter (makes no sense in path),
761            // apply it after basic settings have been applied.
762            final String zc = request.getParameter(NAME_ZOOM_CONTROL_FIELD);
763            if(zc != null)
764                {
765                // Look for +/- adjustment...
766                if("-".equals(zc)) { setZoomFactor(getZoomFactor() - 1); }
767                else if("+".equals(zc)) { setZoomFactor(getZoomFactor() + 1); }
768                // Else try to interpret as rebased zoom factor (1 == MIN).
769                else
770                    {
771                    try { setZoomFactor(Integer.parseInt(zc, 10) + AEUtils.MIN_ZOOM - 1); }
772                    catch(final NumberFormatException e) { } // Ignore unparseable value.
773                    }
774                }
775    
776            // If there are (both) map-click (x,y) parameters are set and usable,
777            // apply after basic settings have been applied.
778            try
779                {
780                final int x = Integer.parseInt(request.getParameter(NAME_MAP_FIELD + ".x"), 10);
781                final int y = Integer.parseInt(request.getParameter(NAME_MAP_FIELD + ".y"), 10);
782    
783                // Only try to use the values if in range...
784                if((x >= 0) && (y >= 0) &&
785                    (x < AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH) &&
786                    (y < AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT))
787                    { adjustCentreWithXYClick(x, y); }
788    
789                // Now adjust the zoom.
790                setZoomFactor(getZoomFactor() + MAP_CLICK_ZF_ADJ);
791                }
792            catch(final NumberFormatException e) { } // No usable map-click parameters.
793    
794    //System.out.println("AEParams="+this);
795            return(this);
796            }
797    
798        /**Set the best view to see the specified Location.
799         * Used to choose the view for a given area,
800         * eg in a link from a catalogue page.
801         * <p>
802         * This sets the (unconstrained) centre to be as close as possible
803         * to the specified location's centre.
804         * <p>
805         * This zooms in as far as it can to still totally enclose the target area
806         * in the putative map vewport.
807         */
808        public AEParams setLocation(final Location.Estd loc)
809            {
810            if(loc == null) { return(this); }
811    
812            // Set the centroid as close as we can.
813            setEastOffset(Math.round((loc.getE().value.floatValue() / 360.0f) * AEUtils.BASE_2D_EARTH_MAP_WIDTH));
814            setNorthOffset(Math.round((loc.getN().value.floatValue() / 180.0f) * AEUtils.BASE_2D_EARTH_MAP_HEIGHT));
815    
816            // Zoom in as far as possible to (roughly) contain the target area.
817            setZoomFactor(AEUtils.MIN_ZOOM);
818            do
819                {
820                // Make sure that putative zoomed-in (constrained) viewport
821                // would still completely enclose target location.
822                final int newZF = getZoomFactor() + 1;
823                if(!makeClone().setZoomFactor(newZF).getLocation(true).containsArea(loc))
824                    { break; }
825    
826                // OK, new zoom would be fine, it seems, so do it...
827                setZoomFactor(newZF);
828                } while(getZoomFactor() < AEUtils.MAX_ZOOM);
829    
830            return(this);
831            }
832    
833        /**Gets parameters suitable to tag on to a URL after a '?', eg for GET operation.
834         * This is an easy way to build a link back to the page.
835         * <p>
836         * Ampersands ("&amp;") separating parameters are expanded to entities,
837         * eg "x=3&amp;amp;y=2" for safety embedded in HTML/XML text.
838         * <p>
839         * Note that this presents unconstrained parameters to preserve visitor's
840         * chosen offset as they zoom in and out.
841         *
842         * @param constrained  if true, values returned are constrained to legal
843         *    (and possibly tile) boundaries, which may help browser caching
844         *    for example
845         */
846        public String getAsGetURLParameters(final boolean constrained)
847            {
848            final StringBuilder result = new StringBuilder(32);
849            result.append(NAME_NORTH_OFFSET).append('=').append(getNorthOffset(constrained));
850                result.append("&amp;");
851            result.append(NAME_EAST_OFFSET).append('=').append(getEastOffset(constrained));
852                result.append("&amp;");
853            result.append(NAME_ZOOM_FACTOR).append('=').append(getZoomFactor());
854            return(result.toString());
855            }
856    
857        /**Gets parameters suitable to tag on to end of servlet URI to make new path.
858         * This is an easy way to build a link back to the page.
859         * <p>
860         * This is a URI suffix of the form "PREFIX/e/n/z"
861         * with no leading or trailing '/'.
862         * <p>
863         * Note that this presents unconstrained parameters to preserve visitor's
864         * chosen offset as they zoom in and out.
865         *
866         * @param constrained  if true, values returned are constrained to legal
867         *    (and possibly tile) boundaries, which may help browser caching
868         *    for example
869         */
870        public String getAsGetPathInfo(final boolean constrained)
871            {
872            final StringBuilder result = new StringBuilder(32);
873            result.append(PARAMS_PREFIX);
874            result.append('/').append(getEastOffset(constrained));
875            result.append('/').append(getNorthOffset(constrained));
876            result.append('/').append(getZoomFactor());
877            return(result.toString());
878            }
879    
880        /**Gets parameters as hidden fields to include in a form.
881         * Note that this presents unconstrained parameters to preserve visitor's
882         * chosen offset as they zoom in and out.
883         */
884        public String getAsHiddenFormElements()
885            {
886            final StringBuilder result = new StringBuilder(128);
887            result.append("<input type=hidden name=").
888                    append(NAME_ZOOM_FACTOR).
889                append(" value=").
890                    append(UploaderUtils.quoteHTMLArg(String.valueOf(getZoomFactor()))).
891                append(">");
892            result.append("<input type=hidden name=").
893                    append(NAME_EAST_OFFSET).
894                append(" value=").
895                    append(UploaderUtils.quoteHTMLArg(String.valueOf(getEastOffset(false)))).
896                append(">");
897            result.append("<input type=hidden name=").
898                    append(NAME_NORTH_OFFSET).
899                append(" value=").
900                    append(UploaderUtils.quoteHTMLArg(String.valueOf(getNorthOffset(false)))).
901                append(">");
902            return(result.toString());
903            }
904    
905        /**Gets parameters as human-readable (albeit terse) String.
906         * Do not attempt to automatically parse this:
907         * the format may change without notice.
908         * <p>
909         * This includes key derived values too.
910         */
911        @Override
912        public String toString()
913            {
914            final StringBuilder sb = new StringBuilder(64);
915            sb.append("AEParams:");
916            sb.append("zf=").append(getZoomFactor()).append(',');
917            sb.append("eo=").append(getEastOffset(false)).append(',');
918            sb.append("no=").append(getNorthOffset(false)).append(';');
919            // Add derived values.
920    //        sb.append("sx=").append(getEastOffsetSrcPixelsX(false)).append(',');
921    //        sb.append("sy=").append(getNorthOffsetSrcPixelsY(false)).append(',');
922    //        sb.append("sw=").append(getBaseSrcPixelsWidth(false)).append(',');
923    //        sb.append("sh=").append(getBaseSrcPixelsHeight(false));
924            sb.append(getLocation(true));
925            return(sb.toString());
926            }
927    
928        /**Returns true if all values are as for initial state.
929         * This can be used to check that it is as if a visitor has newly
930         * arrived at the page with no parameters set, so the zoom, etc,
931         * are left at their default values.
932         */
933        public boolean inDefaultState()
934            {
935            // Check zoom, etc.
936            return((getZoomFactor() == AEUtils.INITIAL_ZOOM) &&
937                   (getEastOffset(false) == 0) &&
938                   (getNorthOffset(false) == 0));
939            }
940    
941        /**Clone this object; all state is separated from the original.
942         */
943        @Override
944        public Object clone()
945            {
946            // All fields can be copied as is; no deep copy is needed.
947            try {
948                final Object result = super.clone();
949                return(result);
950                }
951            catch(final CloneNotSupportedException e)
952                { throw new Error("internal error"); }
953            }
954    
955        /**Make copy of this object; like clone() except no need to cast result.
956         * This is intended to be used with an idiom such as:
957         * <pre>
958         *     makeCopy().setZoomFactor(newValue).getAsGetURLParameters()
959         * </pre>
960         * to get a URL fragment for a link to change the zoom level.
961         */
962        public AEParams makeClone()
963            { return((AEParams) clone()); }
964    
965        /**Makes a clone and steps it by the requested number of tile units in each direction (East and North).
966         * Useful utility for JSP/servlet.
967         * <p>
968         * This coerces offsets to constrained values,
969         * which may help, for example, with cacheing by browsers and in the server,
970         * especially when tiling is being enforced.
971         */
972        public AEParams cloneAndTranslate(final int eTiles, final int nTiles)
973            {
974            final AEParams result = makeClone();
975            result.setEastOffset(result.getEastOffset(true) + eTiles * result.getTileSrcPixelsWidth());
976            result.setEastOffset(result.getEastOffset(true));
977            result.setNorthOffset(result.getNorthOffset(true) + nTiles * result.getTileSrcPixelsHeight());
978            result.setNorthOffset(result.getNorthOffset(true));
979            return(result);
980            }
981    
982        /**Makes an immutable canonical key for use in a hashtable; never null.
983         * This is most valuable when FORCE_TILE_POSITIONING is true.
984         * <p>
985         * The result is produced from the zoom and unconstrained East and North
986         * offset values.  This is unconstrained to allow for different target
987         * centroids even within a single view/tile.
988         * <p>
989         * The result supports equals() and hashCode but is otherwise
990         * to be considered opaque.
991         *
992         * @param constrained  if true then the key is constrained to a
993         *     coarser-grained grid of viewport coordinates
994         */
995        public Object makeKey(final boolean constrained)
996            {
997            // First we normalise the values to be canonical and non-negative.
998            final int normZF = getZoomFactor() - AEUtils.MIN_ZOOM;
999            final int normEO = getEastOffset(constrained) + OFFSET_LIMIT_EAST;
1000            final int normNO = getNorthOffset(constrained) + OFFSET_LIMIT_NORTH;
1001    
1002            // Then work out the multipliers given storage order
1003            // from most to least significant:
1004            //     zf, eo, no
1005            // and need to accommodate full range of value in each "column".
1006            final int multNO = 1;
1007            final int multEO = multNO * (1 + 2*OFFSET_LIMIT_NORTH);
1008            final int multZF = multEO * (1 + 2*OFFSET_LIMIT_EAST);
1009    
1010            final int result =
1011                (normZF * multZF) +
1012                (normEO * multEO) +
1013                (normNO * multNO);
1014    
1015            // Return fast, compact, immutable result supporting equals/hashCode.
1016            return(new Integer(result));
1017            }
1018    
1019        /**Unique Serialisation class ID generated by http://random&#46;hd&#46;org/. */
1020        private static final long serialVersionUID = 3079086832160645496L;
1021        }