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 ("&") separating parameters are expanded to entities,
837 * eg "x=3&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("&");
851 result.append(NAME_EAST_OFFSET).append('=').append(getEastOffset(constrained));
852 result.append("&");
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.hd.org/. */
1020 private static final long serialVersionUID = 3079086832160645496L;
1021 }