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.awt.geom.AffineTransform;
035 import java.awt.image.AffineTransformOp;
036 import java.awt.image.BufferedImage;
037 import java.io.IOException;
038 import java.util.ArrayList;
039 import java.util.Collections;
040 import java.util.Comparator;
041 import java.util.HashMap;
042 import java.util.List;
043 import java.util.Map;
044
045 import javax.servlet.ServletContext;
046
047 import org.hd.d.pg2k.svrCore.CoreConsts;
048 import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
049 import org.hd.d.pg2k.svrCore.ExhibitName;
050 import org.hd.d.pg2k.svrCore.ExhibitPropsComputable;
051 import org.hd.d.pg2k.svrCore.ExhibitPropsComputableMutable;
052 import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
053 import org.hd.d.pg2k.svrCore.GenUtils;
054 import org.hd.d.pg2k.svrCore.ImageUtils;
055 import org.hd.d.pg2k.svrCore.Name;
056 import org.hd.d.pg2k.svrCore.TextUtils;
057 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
058 import org.hd.d.pg2k.svrCore.location.Location;
059 import org.hd.d.pg2k.svrCore.location.LocationMap;
060 import org.hd.d.pg2k.svrCore.props.GenProps;
061 import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
062 import org.hd.d.pg2k.webSvr.location.LocationUtils;
063 import org.hd.d.pg2k.webSvr.util.WebConsts;
064 import org.hd.d.pg2k.webSvr.util.WebUtils;
065
066 import ORG.hd.d.IsDebug;
067
068 /**
069 * Created by IntelliJ IDEA.
070 * User: Damon Hart-Davis
071 * Date: 13-Sep-2003
072 * Time: 21:46:23
073 */
074
075 /**Support for Aloha Earth virtual site.
076 */
077 public final class AEUtils
078 {
079 // /**Site-relative URL to "page" at which Aloha Earth map HTML is shown.
080 // * This is used to allow the Aloha Earth page to easily refer
081 // * to itself by name, for example as the target of a form submission.
082 // */
083 // public static final String AE_SELF_LRURL = "coords/";
084
085 /**Root-relative URL to "page" at which Aloha Earth is shown.
086 * This is used to allow the Aloha Earth page to easily refer
087 * to itself by name, for example as the target of a form submission.
088 */
089 public static final String AE_SELF_RRURL =
090 WebConsts.VIRTUAL_SITE_CUST_ALOHAEARTH_ROOT /* + AE_SELF_LRURL */;
091
092 /**Root-relative URL or source/base 2D Earth image for clickable map.
093 * This is assumed to be a smallish low-colour image
094 * on which we can write red and white markings that will be visible
095 * and which we convert back to a clear and small palette-based image
096 * in a popular and widely-supported format such as GIF or PNG.
097 */
098 public static final String BASE_2D_EARTH_MAP_RRURL =
099 WebConsts.VIRTUAL_SITE_CUST_ALOHAEARTH_ROOT + "Earth-whole-planet.gif";
100
101 /**Width of base image (pixels). */
102 public static final int BASE_2D_EARTH_MAP_WIDTH = 4320;
103
104 /**Height of base image (pixels). */
105 public static final int BASE_2D_EARTH_MAP_HEIGHT = 2160;
106
107
108 /**Root-relative URL to base directory on which map image servlet is mounted.
109 * This must match entry in web.xml.
110 * <p>
111 * Note that this contains a leading and trailing '/'.
112 * <p>
113 * Any URI under this point will be directed to the servlet,
114 * so we can pick the "expected" suffix to avoid surprising broswers
115 * with the MIME type of the downloaded image.
116 * <p>
117 * Because these generated images may have to change the exhibit set
118 * or meta data may change it it not appropriate for this to be under
119 * /_static.
120 */
121 public static final String MAPIMG_SERVLET_MOUNT_DIR =
122 WebConsts.VIRTUAL_SITE_CUST_ALOHAEARTH_ROOT + "mapimg/";
123
124 /**Type of clickable-map image that we dynamically generate.
125 * Need not be the same as the raw base image,
126 * as long as it can (efficiently) encode the modified image
127 * in a byte-indexed format.
128 * <p>
129 * Good types to use are PNG and GIF (when the LZW patent expires).
130 */
131 static final int DYN_MAP_IMG_TYPE = ExhibitMIME.ET_PNG;
132
133 /**ExhibitMIME entry for the chosen outout type. */
134 static final ExhibitMIME.ExhibitTypeParameters ETP =
135 ExhibitMIME.getParamsByType(DYN_MAP_IMG_TYPE);
136
137 /**Full root-relative URL given for map image servlet.
138 * The extension is chosen to match the MIME type.
139 * <p>
140 * We will append GET-style parameters to allow the appropriate
141 * zoom and shift to be applied.
142 */
143 public static final String MAP_SERVLET_RRURL =
144 MAPIMG_SERVLET_MOUNT_DIR + "Earth" + ETP.dotSuffixForInputFile;
145
146
147 /**Zoom ratio: what factor we zoom in/out each time; at least 2.
148 * Also determines size of lateral (up/down/left/right) step
149 * as fraction of width of whole image as shown to the user.
150 * <p>
151 * A value of 2 or 3 is probably good; up to 8 is probably OK.
152 */
153 public static final int ZOOM_RATIO = 2;
154
155 /**Minimum zoom ratio from initial base image.
156 * A negative value means the image can be zoomed out (shrunk)
157 * relative to the base image.
158 * <p>
159 * This is in terms of zooms in or out
160 * (changes in each dimension) by ZOOM_RATIO.
161 */
162 public static int MIN_ZOOM = -3;
163
164 /**Maximum zoom ratio from initial base image.
165 * A positive value means the image can be zoomed in (expanded)
166 * relative to the base image. This will determine how many
167 * pixels on a side a single source pixel can be expanded to.
168 * <p>
169 * This is in terms of zooms in or out
170 * (changes in each dimension) by ZOOM_RATIO.
171 * <p>
172 * This must be no less than MIN_ZOOM,
173 * and is usually positive.
174 * <p>
175 * Since this effectively limits the precision with which a user can
176 * select an area to view on the one hand, and yet increases the
177 * 'chunkiness' of the maximally zoomed image, is is difficult to chose
178 * an optimal value, but one that expand source pixels to no more
179 * than about 8--16 pixels on a side is probably reasonable.
180 */
181 public static int MAX_ZOOM = +5;
182
183 /**The initial zoom ratio as seen by a visitor.
184 * The raw image is zoomed by this amount and this determines
185 * the size of the derived clickable map to be shown to the visitor
186 * at all levels of zoom.
187 * <p>
188 * This is usually but not always the same as the MIN_ZOOM,
189 * but must be in the inclusive range
190 * [MIN_ZOOM, MAX_ZOOM].
191 */
192 public static int INITIAL_ZOOM = MIN_ZOOM;
193
194 /**Apply zoom to value in pixels.
195 * A negative value reduces the value (brings it towards zero)
196 * and a positive one increases it;
197 * zero returns the value unchanged.
198 * <p>
199 * This computes input * (ZOOM_RATIO ^ zoomFactor).
200 */
201 public static int applyZoom(int pixels, int zoomFactor)
202 {
203 // A zoom factor of zero is the identity function.
204 if(zoomFactor == 0) { return(pixels); }
205
206 if(zoomFactor > 0)
207 {
208 // Special case for power of two.
209 if(ZOOM_RATIO == 2)
210 { return(pixels << zoomFactor); }
211
212 // General case.
213 while(--zoomFactor >= 0) { pixels *= ZOOM_RATIO; }
214 return(pixels);
215 }
216
217
218 // Else zoomFactor < 0.
219
220 // Special case for power of two.
221 if(ZOOM_RATIO == 2)
222 { return(pixels >> -zoomFactor); }
223
224 // Avoid repeated division for speed
225 // and to try to avoid unnecessary loss of information/accuracy.
226 int divisor = 1;
227 while(++zoomFactor <= 0) { divisor *= ZOOM_RATIO; }
228 pixels /= divisor;
229 return(pixels);
230 }
231
232 /**Display width of image to show visitor.
233 * Determined by original image width and INITIAL_ZOOM.
234 */
235 public static final int DISPLAY_2D_EARTH_MAP_WIDTH =
236 applyZoom(BASE_2D_EARTH_MAP_WIDTH, INITIAL_ZOOM);
237
238 /**Display height of image to show visitor.
239 * Determined by original image height and INITIAL_ZOOM.
240 */
241 public static final int DISPLAY_2D_EARTH_MAP_HEIGHT =
242 applyZoom(BASE_2D_EARTH_MAP_HEIGHT, INITIAL_ZOOM);
243
244
245 /**Make scaled base image fragment given Rectangle bounding source pixels.
246 * Returns writable ARGB true colour image fragment suitable to draw on.
247 * <p>
248 * May consume significant resources (CPU and memory),
249 * so access and concurrency should be controlled.
250 *
251 * @param srcRect as returned by AEParams.getSourceRectangleToDisplay
252 * @param context servlet context to retrieve base image from
253 */
254 static BufferedImage makeScaledBaseMapFragment(final Rectangle srcRect,
255 final ServletContext context)
256 {
257 final BufferedImage imageIn =
258 WebUtils.getAndCacheStaticImage(false, // We'll copy below.
259 BASE_2D_EARTH_MAP_RRURL,
260 false, // Don't force to memory-hogging ARGB true-colour here!
261 context,
262 false); // Cache source image indefinitely.
263
264 //System.out.println("makeScaledBaseMapFragment("+srcRect+", ctxt)");
265
266 // Extract the map image fragment that we will be copying from.
267 // Avoid copying any data...
268 final BufferedImage srcFragment = imageIn.getSubimage(
269 srcRect.x, srcRect.y, srcRect.width, srcRect.height);
270
271 // Make a scaled copy of the original image.
272 // We use nearest-neighbour scaling because:
273 // * It is fast.
274 // * It should work for indexed-colour (and other) images
275 // (eg this does not create pixels with new colours).
276 final AffineTransformOp op = new AffineTransformOp(
277 AffineTransform.getScaleInstance(
278 DISPLAY_2D_EARTH_MAP_WIDTH / (double) srcRect.width,
279 DISPLAY_2D_EARTH_MAP_HEIGHT / (double) srcRect.height),
280 AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
281 final BufferedImage scaledImage = op.filter(srcFragment, null);
282
283 // Convert to ARGB true-colour to be easily drawn on, if necessary.
284 final BufferedImage result = ImageUtils.convertToTrueColourARGB(scaledImage, false);
285
286 return(result);
287 }
288
289
290 /**Colour of marker to show location of exhibit on map, in RGB format.
291 * Since the map is usually rendered in green (land) and blue (sea)
292 * this should probably be red or some other high-contrast colour.
293 * This should probably also be fully opaque to minimise encoded image size
294 * (eg 0xffff0000 for opaque red).
295 */
296 static final int EXHIBIT_MARKER_RGB_COLOUR =
297 LocationUtils.DYN_LOC_TN_CROSSHAIR_RGB_COLOUR;
298
299 /**Colour of marker to show area indicated by title, in RGB format.
300 * Since the map is usually rendered in green (land) and blue (sea)
301 * and individual exhibits in red
302 * this should probably be orange or some other nearish-red colour.
303 * This should probably also be fully opaque to minimise encoded image size
304 * (eg 0xffff0000 for opaque red).
305 */
306 static final int TITLE_AREA_MARKER_RGB_COLOUR = 0xffE47833;
307
308
309
310 /**Category whose LocationMap entries are used by getViewLocationMapVirtualPrefix(); not null. */
311 private static final Name _GVT_CATEGORY = Name.create("places-and-sights");
312 /**Lookup prefix; not null. */
313 private static final Name _GVT_CATEGORY_LU_PREFIX = Name.create(_GVT_CATEGORY.toString() + '/', _GVT_CATEGORY);
314
315 /**This gets a virtual prefix from the LocationMap for the current view or empty String if none suitable; never null.
316 * This returns the prefix the best possible match for the current view,
317 * or returns ""/EMPTY if it cannot find anything suitable.
318 * The prefix returned does contain the category,
319 * and is of the form dir/word-{word-}*,
320 * eg "places-and-sights/England-London-".
321 * <p>
322 * This should always return "" at the outermost level.
323 * <p>
324 * This looks up places-and-sights prefixes in the given LocationMap,
325 * assuming that each such prefix is a reasonable region.
326 * This tries a number of methods to find the "best" match,
327 * in this order (excluding any "whole Earth" match:
328 * <ol>
329 * <li>The smallest location prefix wholly contained in the current view
330 * <em>that contains the user's selected target centroid</a>.
331 * <li>The largest location prefix wholly contained in the current view.
332 * <li>The smallest such Estd entry that wholly contains the current view.
333 * <li>The smallest such Estd entry whose centre is in the current view.
334 * <ol>
335 *
336 * @param aeps parameters with which map to display is chosen; not null
337 * @param lm map to do lookup in; not null
338 *
339 * @return virtual prefix lookup into
340 * LocationMap.getMapFromNamePrefixToLocation() or "" if none
341 */
342 public static Name getViewLocationMapVirtualPrefix(final AEParams aeps,
343 final LocationMap lm)
344 {
345 // Optimisation: at max zoom just return empty title immediately.
346 if(aeps.getZoomFactor() == MIN_ZOOM)
347 { return(Name.EMPTY); }
348
349 // Get prefix map.
350 final Map<Name,Location.Base> prefixMap = lm.getMapFromNamePrefixToLocation();
351 // Get current view area as seen on the screen and as requested...
352 final Location.Estd viewArea = aeps.getLocation(true);
353 final Location.Estd viewAreaUnconstrained = aeps.getLocation(false);
354
355 // Current best result.
356 Name result = null;
357 // Area of current best result.
358 Location.Estd resultArea = null;
359
360
361 // Look for smallest contained prefix/area, if any,
362 // CONTAINING THE CURRENT (UNCONSTRAINED) TARGET CENTROID.
363 for(final Name prefix : prefixMap.keySet())
364 {
365 final Location.Estd area = (Location.Estd) prefixMap.get(prefix);
366 // Screen for usability.
367 if(!_screenForGetViewTitle(prefix, area)) { continue; }
368
369 // If area not completely contained by view, skip it.
370 if(!viewArea.containsArea(area)) { continue; }
371
372 // If this area does not contain the target centroid, skip it.
373 if(!area.containsCentre(viewAreaUnconstrained)) { continue; }
374
375 // If this is smaller than the previous best,
376 // or there is no previous best,
377 // then take it.
378 if((resultArea == null) || resultArea.isLargerThan(area))
379 {
380 result = prefix;
381 resultArea = area;
382 }
383 }
384 // If we found an enclosed area, return prefix.
385 if(result != null) { return(result); }
386
387
388 // Look for largest contained prefix/area, if any.
389 for(final Name prefix : prefixMap.keySet())
390 {
391 final Location.Estd area = (Location.Estd) prefixMap.get(prefix);
392 // Screen for usability.
393 if(!_screenForGetViewTitle(prefix, area)) { continue; }
394
395 // If area not completely contained by view, skip it.
396 if(!viewArea.containsArea(area)) { continue; }
397
398 // If this is larger than the previous best,
399 // or there is no previous best,
400 // then take it.
401 if((resultArea == null) || area.isLargerThan(resultArea))
402 {
403 result = prefix;
404 resultArea = area;
405 }
406 }
407 // If we found an enclosed area, return prefix.
408 if(result != null) { return(result); }
409
410
411 // Look for smallest enclosing prefix/area, if any.
412 for(final Name prefix : prefixMap.keySet())
413 {
414 final Location.Estd area = (Location.Estd) prefixMap.get(prefix);
415 // Screen for usability.
416 if(!_screenForGetViewTitle(prefix, area)) { continue; }
417
418 // If area does not completely contain view, skip it.
419 if(!area.containsArea(viewArea)) { continue; }
420
421 // If this is smaller than the previous best,
422 // or there is no previous best,
423 // then take it.
424 if((resultArea == null) || resultArea.isLargerThan(area))
425 {
426 result = prefix;
427 resultArea = area;
428 }
429 }
430 // If we found an enclosing area, return prefix.
431 if(result != null) { return(result); }
432
433
434 // Look for smallest prefix/area whose centre is in view, if any.
435 for(final Name prefix : prefixMap.keySet())
436 {
437 final Location.Estd area = (Location.Estd) prefixMap.get(prefix);
438 // Screen for usability.
439 if(!_screenForGetViewTitle(prefix, area)) { continue; }
440
441 // If view does not contain centre of area, skip it.
442 if(!viewArea.containsCentre(area)) { continue; }
443
444 // If this is smaller than the previous best,
445 // or there is no previous best,
446 // then take it.
447 if((resultArea == null) || resultArea.isLargerThan(area))
448 {
449 result = prefix;
450 resultArea = area;
451 }
452 }
453 // If we found an enclosing area, return prefix.
454 if(result != null) { return(result); }
455
456 return(Name.EMPTY); // Nothing found.
457 }
458
459 /**Screens entry from Location map; returns true if usable.
460 * Returns true if:
461 * <ul>
462 * <li>Name indicates a places-and-sights entry.
463 * <li>The area is smaller than whole Earth area.
464 * </ul>
465 */
466 private static boolean _screenForGetViewTitle(final Name prefix,
467 final Location.Estd area)
468 {
469 // If not a places-and-sights prefix, ignore it.
470 if(!TextUtils.startsWith(prefix, _GVT_CATEGORY_LU_PREFIX)) { return(false); }
471
472 // Explicitly reject items that have area of whole Earth,
473 // ie don't carry much information as labels.
474 if((area.getN().error.intValue() >= Location.Estd.MAX_N) &&
475 (area.getE().error.intValue() >= Location.Estd.MAX_E))
476 { return(false); }
477
478 return(true); // Seems OK.
479 }
480
481
482 /**True if labels run horizontally, false if vertical. */
483 public static final boolean LABEL_HORIZ = true;
484
485 /**Label clearance (ie approximately the typical text height or 1em) in pixels; strictly positive. */
486 public static final int LABEL_CLEARANCE_PX = 20;
487
488 /**Overlay thumbnail clearance is longest (small) thumbnail dimension. */
489 public static final int LABEL_TN_CLEARANCE_PX = ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX;
490
491 /**Maximum number of labels to show; strictly positive.
492 * This could be partially determined by the map image size,
493 * ie how many labels can be comfortably shown.
494 */
495 public static final int LABEL_COUNT_MAX = Math.max(5, Math.min(20,
496 (LABEL_HORIZ ? DISPLAY_2D_EARTH_MAP_HEIGHT : DISPLAY_2D_EARTH_MAP_WIDTH) /
497 (2 * LABEL_CLEARANCE_PX)));
498
499 /**Gets locationMap keys for labels to add to the map; result may be empty but is never null.
500 * More important labels are earlier in the list.
501 * <p>
502 * We always try to include the title label, if available.
503 * <p>
504 * We then count how many uses of each prefix there are in this view,
505 * and essentially return the top LABEL_COUNT_MAX of them,
506 * but exclude any that are too close to any already selected
507 * (and maybe those that are too near an edge to be read properly).
508 * <p>
509 * We are only interested in items wholly contained in the current view.
510 *
511 * @return best-first list of prefixes thumbnails; never null
512 */
513 public static List<Name> getViewLocationMapLabelPrefixes(
514 final AEParams aeps,
515 final LocationMap lm,
516 final AlohaEarthMapCache cache)
517 {
518 if((aeps == null) || (lm == null) || (cache == null))
519 { throw new IllegalArgumentException(); }
520
521 final long startTime = System.currentTimeMillis();
522
523 final List<Name> result = new ArrayList<Name>(LABEL_COUNT_MAX);
524
525 // Return the heading prefix/label first if available
526 // and regardless of its position on the map.
527 final Name headingPrefix = getViewLocationMapVirtualPrefix(aeps, lm);
528 if(0 != headingPrefix.length()) { result.add(headingPrefix); }
529
530 final Location.Estd currentView = aeps.getLocation(true);
531
532 // Count the number of exhibits in this view with each location.
533 // The keys are Location.Estd instances which, for this to work,
534 // we assume to have useful equality and/or have been intern()ed.
535 // The values are instances of int[1] as counters.
536 // When done we try to map back from Location to prefix.
537 final Map<Location.Estd, int[]> counts = new HashMap<Location.Estd, int[]>();
538 final List<Name.ExhibitFull> exhibits = cache.selectContainedExhibits(aeps);
539 for(final Name.ExhibitFull ex : exhibits)
540 {
541 final Location.Base location = cache.aep.getLocation(ex);
542 if(!(location instanceof Location.Estd)) { continue; }
543 final Location.Estd locE = (Location.Estd) location;
544
545 // Ignore items not enclosed in the current viewport.
546 if(!currentView.containsArea(locE)) { continue; }
547
548 int[] count = counts.get(locE);
549 if(count == null)
550 {
551 count = new int[1];
552 counts.put(locE, count);
553 }
554 ++count[0];
555 }
556
557 if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): counts.size(): "+(counts.size())+".]"); }
558
559 // Get list of all the Locations found.
560 final ArrayList<Location.Estd> locations = new ArrayList<Location.Estd>(counts.keySet());
561 // Sort into order by count, highest count first.
562 Collections.sort(locations, new Comparator<Location.Estd>()
563 {
564 public final int compare(final Location.Estd estd1, final Location.Estd estd2)
565 {
566 final int count1 = counts.get(estd1)[0];
567 final int count2 = counts.get(estd2)[0];
568 if(count1 > count2) { return(-1); } // Correct order.
569 if(count1 < count2) { return(+1); } // Wrong order.
570 return(0);
571 }
572 });
573
574 // We will use the location lookup maps both ways...
575 final Map<Name,Location.Base> mapFromNamePrefixToLocation = lm.getMapFromNamePrefixToLocation();
576 final Map<Location.Base,Name> mapFromLocationToNamePrefix = lm.getMapFromLocationToNamePrefix();
577
578 // Go through locations in descending-count order,
579 // picking those that do not clash with existing items,
580 // until we hit our label-count limit.
581 vetLocations: for(final Location.Estd l : locations)
582 {
583 if(result.size() >= LABEL_COUNT_MAX) { break; }
584
585 final Name prefix = mapFromLocationToNamePrefix.get(l);
586 if(prefix == null)
587 {
588 if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): no prefix for location "+l+", COUNT="+(counts.get(l)[0])+".]"); }
589 continue;
590 }
591
592 // Avoid duplicates, eg of the title prefix.
593 if(result.contains(prefix))
594 {
595 //if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): already have prefix "+prefix+".]"); }
596 continue;
597 }
598
599 final Point displayPixel = aeps.getDisplayPixelForEstdLocationCentre(l);
600
601 // // Avoid anything too close to any edge...
602 // if(displayPixel == null)
603 // {
604 //if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): cannot compute display pixel for location "+l+".]"); }
605 // continue;
606 // }
607 // if((displayPixel.x < LABEL_CLEARANCE_PX) ||
608 // (displayPixel.y < LABEL_CLEARANCE_PX) ||
609 // (displayPixel.x >= DISPLAY_2D_EARTH_MAP_WIDTH - LABEL_CLEARANCE_PX) ||
610 // (displayPixel.y >= DISPLAY_2D_EARTH_MAP_HEIGHT - LABEL_CLEARANCE_PX))
611 // {
612 ////if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): too close to edge: "+prefix+".]"); }
613 // continue;
614 // }
615
616 // Avoid too close vertically or horizontally to any extant result.
617 // This avoids the labels colliding with whatever length/alignment,
618 // whether written vertically or horizontally.
619 for(final Name r : result)
620 {
621 final Location.Base loc = mapFromNamePrefixToLocation.get(r);
622 assert(loc != null);
623 assert(loc instanceof Location.Estd);
624 final Location.Estd locE = (Location.Estd) loc;
625 final Point dp = aeps.getDisplayPixelForEstdLocationCentre(locE);
626 assert(dp != null);
627 if((Math.abs(dp.x - displayPixel.x) < LABEL_CLEARANCE_PX) ||
628 (Math.abs(dp.y - displayPixel.y) < LABEL_CLEARANCE_PX))
629 {
630 //if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): too close to exiting result: "+prefix+".]"); }
631 continue vetLocations;
632 }
633 }
634
635 // Looks OK to add this prefix to the results...
636 result.add(prefix);
637 }
638
639 if(IsDebug.isDebug) { System.out.println("[AEUtils.getViewLocationMapLabelPrefixes(): result.size(): "+(result.size())+", time="+(System.currentTimeMillis() - startTime)+"ms.]"); }
640
641 return(result);
642 }
643
644 /**Preamble for label overlay HTML. */
645 private static final String LABEL_HTML_PREAMBLE = "<ul id=\"labels\" style=\"list-style-type:none; position:absolute; left:0; top:0; width:0; height:0; margin:0; padding:0;\">";
646
647 /**Postamble for label overlay HTML. */
648 private static final String LABEL_HTML_POSTAMBLE = "</ul>";
649
650 /**If true then do a "poor-man's drop-shadow" to help improve contrast. */
651 private static final boolean LABEL_PMDS = true;
652
653 /**Create HTML label text to overlay the map; returns "" if none, never null.
654 * This creates an unnumbered list with one or more absolutely-positioned
655 * (within the encompassing relatively positioned parent element).
656 * If there are no labels to show then this does not create the UL element
657 * and the result is "".
658 * <p>
659 * The first view labels may be rendered more strongly.
660 */
661 public static String getViewLocationLabels(final AEParams aeps,
662 final DataSourceBean dsb,
663 final AlohaEarthMapCache aemfb,
664 final boolean doThumbnails)
665 {
666 final long startTime = System.currentTimeMillis();
667
668 final LocationMap lm = aemfb.aep.epgi.getLocationMap();
669
670 final List<Name> prefixes = getViewLocationMapLabelPrefixes(aeps, lm, aemfb);
671 if(prefixes.isEmpty()) { return(""); }
672
673 final Map<Name,Location.Base> mapFromNamePrefixToLocation = lm.getMapFromNamePrefixToLocation();
674 final int capacity = LABEL_HTML_PREAMBLE.length() + LABEL_HTML_POSTAMBLE.length() +
675 (LABEL_PMDS?512:256)*(doThumbnails?2:1)*prefixes.size();
676 final StringBuilder sb = new StringBuilder(capacity);
677
678 final List<Name.ExhibitFull> exhibitsForTNs = doThumbnails ? aemfb.selectContainedExhibits(aeps) : null;
679
680 // Allow a little local computation time,
681 // but not so much as to drive users mad, we hope...
682 final long localStart = System.currentTimeMillis();
683 final long stopBy = localStart + CoreConsts.MAX_INTERACTIVE_DELAY_MS +
684 (localStart - startTime); // Spend at least as long as our subroutines did...
685
686 // True until we've gathered at least one requested thumbnail...
687 boolean stillToGetThumbnail = doThumbnails;
688
689 // Generate HTML for each label...
690 int ordinal = 0;
691 for(final Name prefix : prefixes)
692 {
693 final Location.Base loc = mapFromNamePrefixToLocation.get(prefix);
694 if(!(loc instanceof Location.Estd)) { continue; }
695 final Location.Estd eloc = ((Location.Estd) loc);
696 final Point p = aeps.getDisplayPixelForEstdLocationCentre(eloc);
697 if(p == null) { continue; }
698
699 // Emphasise first few labels...
700 final boolean strong = (++ordinal <= 3);
701
702 // Compute label link target.
703 final String labelHref = AEUtils.AE_SELF_RRURL + (new AEParams()).setLocation(eloc).getAsGetPathInfo(false);
704
705 // Offset label very slightly so as not to obscure the target.
706 final int leftPX = p.x + 1;
707 final int topPX = p.y + 1;
708 // Use part after the '/' as the label,
709 // and strip off the trailing '-' too.
710 final String labelText = prefix.subSequence(1 + TextUtils.indexOf(prefix, '/'),
711 prefix.length()-1).toString();
712
713 String imgHTML = null;
714 if(doThumbnails &&
715 (stillToGetThumbnail || (System.currentTimeMillis() <= stopBy)))
716 {
717 GenProps gp = new GenProps();
718 try { gp = dsb.getGenProps(-1); } catch(final IOException e) { }
719
720 // Best thumbnail candidate found so far; null if none.
721 Name.ExhibitFull bestSoFar = null;
722 // Score of best so far; initially neutral.
723 int bestScore = 0;
724
725 // Find (first) good (small) thumbnail image for each label.
726 for(final Name.ExhibitFull exhibitName : exhibitsForTNs)
727 {
728 // If this exhibit can't have a thumbnail then skip it.
729 if(!(ExhibitMIME.getInputFileType(aemfb.aep.aeid.getStaticAttr(exhibitName).getCharSequence())).canPossiblyCreateThumbnailOfSameMIMEType())
730 { continue; }
731 //System.out.println("CAN HAVE TN: " + exhibitName);
732 // Skip if this exhibit does not have the right location.
733 if(!eloc.equals(aemfb.aep.getLocation(exhibitName)))
734 { continue; }
735 //System.out.println("CORRECT LOC: " + exhibitName);
736 // If this exhibit is "sensitive" then skip it.
737 if(GenUtils.isSensitive(exhibitName, gp))
738 { continue; }
739 // Skip if this exhibit is not notably good,
740 // or at least the best non-bad one so far!
741 // Don't necessarily force goodness value to be computed.
742 final boolean runningLate = System.currentTimeMillis() >= stopBy;
743 final ExhibitPropsComputableMutable epcm = aemfb.aep.getExhibitPropsComputableMutable(exhibitName, runningLate, gp, dsb, dsb.getScorerCache());
744 final int goodness = epcm.getGoodness();
745 // Skip anything not at least as that we already have...
746 if(goodness <= bestScore)
747 { continue; }
748 if(IsDebug.isDebug) { System.out.println("BEST SO FAR: " + exhibitName); }
749 final boolean isGood = Boolean.TRUE.equals(epcm.isGood());
750 // Only accept less-than-good if we have nothing at all yet.
751 if((bestSoFar != null) && !isGood)
752 { continue; }
753 if(IsDebug.isDebug) { System.out.println("IS GOOD: " + exhibitName); }
754 if(IsDebug.isDebug) { System.out.println(" RUNNING LATE: " + runningLate); }
755 // Skip if this exhibit does not have a small thumbnail.
756 // If we get here then it may well be worth trying hard,
757 // at least once, to create/fetch a missing thumbnail...
758 // Keep going until we have at least one.
759 final boolean createTN = stillToGetThumbnail || !runningLate;
760 final ExhibitThumbnails thumbnails = dsb.getThumbnails(exhibitName, createTN);
761 if((thumbnails == null) || (thumbnails.getSmall() == null))
762 { continue; }
763 if(IsDebug.isDebug) { System.out.println("HAS SMALL TN: " + exhibitName); }
764
765 // OK, this is the best so far!
766 bestSoFar = exhibitName;
767 bestScore = goodness;
768
769 // We seem to have a candidate...
770 // Create img tag for thumbnail...
771 final ExhibitPropsComputable epc = aemfb.aep.getExhibitPropsComputable(exhibitName);
772 final java.awt.Dimension xyDim = (epc == null) ? null : epc.getXyDimensions();
773 final java.awt.Dimension thumbnailXyDim = ExhibitThumbnails.computeThumbnailDimensions(xyDim, false);
774 final String rrURL = WebUtils.makeThumbnailRRURL(exhibitName, false);
775 final CharSequence mainWordsComponent = ExhibitName.getMainWordsComponent(exhibitName, ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet());
776 imgHTML = "<img src=\"" + rrURL + "\" border=\"0\" " +
777 ((thumbnailXyDim == null) ? "" :
778 "width=\""+thumbnailXyDim.width+"\" height=\""+thumbnailXyDim.height+"\" " +
779 "style=\"position:relative; left:-"+(thumbnailXyDim.width/2)+"px; top:-"+(thumbnailXyDim.height/2)+"px;\" ") +
780 "alt=\"" + mainWordsComponent + "\" " +
781 "title=\"" + mainWordsComponent + "\">";
782
783 if(IsDebug.isDebug) { System.out.println("FIRST TN FOR OVERLAY: " + stillToGetThumbnail); }
784 // We've done at least one thumbnail as requested.
785 stillToGetThumbnail = false;
786
787 // We can stop immediately if this is positively good,
788 // and if we're short of time,
789 // else we'll keep hunting for the "best" image.
790 if(isGood && runningLate)
791 { break; }
792 }
793 }
794
795 // If there's an image then put it in first, partly transparent,
796 // so that we can overlay the label text on top of it.
797 if(imgHTML != null)
798 {
799 sb.append("<li>");
800 if(labelHref != null) { sb.append("<a href=\"").append(labelHref).append("\">"); }
801 sb.append("<span style=\"opacity:").append(strong ? "0.9" : "0.7").append("; ").
802 append("position:absolute; ").
803 append("left:").append(leftPX).append("px; ").
804 append("top:").append(topPX).append("px; ");
805 sb.append("\">");
806 sb.append(imgHTML);
807 sb.append("</span>");
808 if(labelHref != null) { sb.append("</a>"); }
809 sb.append("</li>");
810 }
811
812 if(LABEL_PMDS)
813 {
814 // Create crude drop-shadow to help improve contrast.
815 appendLabelHTML(sb, leftPX+1, topPX+1, strong, labelText, "black", labelHref);
816 }
817
818 // Main label.
819 // Allow label text to use extant page colour scheme.
820 appendLabelHTML(sb, leftPX, topPX, strong, labelText, null, labelHref);
821 }
822
823 if(sb.length() == 0)
824 { return(""); }
825 sb.insert(0, LABEL_HTML_PREAMBLE);
826 sb.append(LABEL_HTML_POSTAMBLE);
827
828 if(IsDebug.isDebug && (sb.length() > capacity)) { System.err.println("WARNING: getViewLocationLabels(): length="+sb.length()+", capacity="+capacity); }
829
830 if(IsDebug.isDebug) { System.out.println("[getViewLocationLabels(): "+(System.currentTimeMillis()-startTime)+"ms.]"); }
831
832 return(sb.toString());
833 }
834
835 /**Generate HTML for one overlay label and append to given StringBuilder.
836 * The text label's content, colour, position and strength is chose.
837 *
838 * @param labelColour colour for label text; null if default
839 * @param labelHref href for label text; null if none
840 */
841 private static void appendLabelHTML(final StringBuilder sb,
842 final int leftPX,
843 final int topPX,
844 final boolean strong,
845 final String labelText,
846 final String labelColour,
847 final String labelHref)
848 {
849 sb.append("<li>");
850 if(labelHref != null) { sb.append("<a href=\"").append(labelHref).append("\">"); }
851 if(strong) { sb.append("<big>"); }
852 sb.append("<span style=\"white-space:nowrap; position:absolute; ").
853 append("left:").append(leftPX).append("px; ").
854 append("top:").append(topPX).append("px; ");
855 if(labelColour != null) { sb.append("color:").append(labelColour).append(";"); }
856 sb.append("\">");
857 sb.append(labelText);
858 sb.append("</span>");
859 if(strong) { sb.append("</big>"); }
860 if(labelHref != null) { sb.append("</a>"); }
861 sb.append("</li>");
862 }
863
864
865 /**Key for AE-linked data in DataSourceBean.
866 * Package-only to keep scope as narrow as possible!
867 */
868 static final DataSourceBean.AEPLinkedKey AEKey = new DataSourceBean.AEPLinkedKey("AlohaEarth");
869
870 /**Get the filter bean associated with the current DataSourceBean; never null.
871 * A new empty value is created on first use and when the AEP changes.
872 */
873 public static AlohaEarthMapCache getAemfb(final DataSourceBean dsb)
874 throws IOException
875 {
876 if(dsb == null) { throw new IllegalArgumentException(); }
877
878 AlohaEarthMapCache aemfb;
879 while((aemfb = (AlohaEarthMapCache) dsb.getAEPLinkedValue(AEKey)) == null)
880 { dsb.putIfAbsentAEPLinkedValue(AEKey, new AlohaEarthMapCache(dsb)); }
881
882 return(aemfb);
883 }
884 }