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.image.BufferedImage;
034 import java.io.IOException;
035 import java.lang.ref.SoftReference;
036 import java.util.Arrays;
037 import java.util.Collections;
038 import java.util.HashSet;
039 import java.util.Hashtable;
040 import java.util.Iterator;
041 import java.util.List;
042 import java.util.Map;
043
044 import javax.servlet.ServletContext;
045
046 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
047 import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
048 import org.hd.d.pg2k.svrCore.ImageUtils;
049 import org.hd.d.pg2k.svrCore.MemoryTools;
050 import org.hd.d.pg2k.svrCore.Name;
051 import org.hd.d.pg2k.svrCore.ROByteArray;
052 import org.hd.d.pg2k.svrCore.TextUtils;
053 import org.hd.d.pg2k.svrCore.location.Location;
054 import org.hd.d.pg2k.svrCore.location.LocationMap;
055 import org.hd.d.pg2k.webSvr.exhibit.BuiltInFilters;
056 import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
057 import org.hd.d.pg2k.webSvr.exhibit.Expr;
058 import org.hd.d.pg2k.webSvr.exhibit.FilterExpr;
059
060 import ORG.hd.d.IsDebug;
061
062 /**
063 * Created by IntelliJ IDEA.
064 * User: Damon Hart-Davis
065 * Date: 23-Sep-2003
066 * Time: 21:39:33
067 */
068
069 /**This class caches Estd-located exhibits and map fragments for Aloha Earth.
070 */
071 public final class AlohaEarthMapCache
072 {
073 /**If true, use SoftReference memory-sensitive cache; use if memory-starved.
074 * We will always cache the top couple of levels which are likely
075 * to be small in number but relatively expensive to recompute.
076 */
077 private static final boolean MEMORY_SENSITIVE_CACHE = true;
078
079 /**Empty exhibits list. */
080 private static final List<Name.ExhibitFull> NO_EXHIBITS = Collections.emptyList();
081
082 /**Emergency-free hook called in case of critical memory shortage.
083 * This calls clear() on each of the Hashtable values that is was constructed with.
084 * <p>
085 * This retains no references to the entire cache, just the clear()able bits.
086 * <p>
087 * This re-registers itself when it has been run.
088 */
089 private static final class EFHook implements MemoryTools.RecurrentEmergencyFreeHandle
090 {
091 private final Hashtable<?,?>[] caches;
092 EFHook(final Hashtable<?,?>[] caches) { this.caches = caches; }
093 public void run()
094 {
095 for(final Hashtable<?,?> h : caches) { h.clear(); }
096 }
097 }
098
099 /**Default constructor.
100 * Sets the filter to be one on location (exhibits with Estd location).
101 * <p>
102 * We expect its lazy computation to take lots of CPU in total,
103 * so we try to hang onto some of the base/expensive cached state,
104 * but we do have an emergency-free hook to release everything in case of emergency.
105 * <p>
106 * Only package-visible since only AlohaEarth utility methods need
107 * to construct this.
108 */
109 AlohaEarthMapCache(final DataSourceBean dsb)
110 throws IOException
111 {
112 if(dsb == null) { throw new IllegalArgumentException(); }
113
114 aep = dsb.getAllExhibitProperties(-1);
115 final Name.ExhibitFull[] result = aep.select(new AllExhibitProperties.AEPFilter(){
116 public final boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
117 {
118 final Location.Base l = aep.getLocation(exhibitName);
119 return(l instanceof Location.Estd);
120 }
121 }, null, 0);
122 allEstdExhibits = Collections.unmodifiableList(Arrays.asList(result));
123
124 // Set up emergency-free hook
125 // for all things that can be safely clear()ed on demand.
126 efh = new EFHook(new Hashtable[]{ _sCE_cache, _sVLL_cache, _sVLVP_cache, _sMEI_cache });
127 MemoryTools.registerRecurrentEmergencyFreeHandle(efh);
128 }
129
130 /**Emergency-free hook, never null.
131 * Reference has to be maintained to prevent instant expiry.
132 */
133 private final EFHook efh;
134
135 /**Underlying AEP; never null. */
136 public final AllExhibitProperties aep;
137
138 /**Unmodifiable smart-sorted list of all exhibits with Estd location; never null. */
139 private final List<Name.ExhibitFull> allEstdExhibits;
140
141 /**If true, we attempt to cache images and exhibit sub-sets by area.
142 * If false, everything is created on demand (which may be slow).
143 * <p>
144 * We only attempt to cache where the constrained offsets of AEParams
145 * are forced to tile increments, ensuring that only a relatively
146 * small number of distinct areas can be seen by the viewer,
147 * reducing the maximum number of distinct cached items and increasing
148 * the potential hit rate.
149 */
150 private static final boolean CACHEING = AEParams.FORCE_TILE_POSITIONING;
151
152 /**If true, we attempt to select exhibits from the zoom-out set.
153 * This is an optimisation to attempt to filter exhibits of an area
154 * from the same offset but zoomed out one step rather than from the
155 * base set of exhibits, which will probably save a lot of time
156 * on all but the outermost view.
157 * <p>
158 * This only make sense if we are cacheing results of lookups,
159 * and if we are constraining lookups to tile boundaries.
160 */
161 private static final boolean RECURSIVE_SELECT = CACHEING &&
162 AEParams.FORCE_TILE_POSITIONING;
163
164 /**If false, the entire set of exhibits selected by the master filter appears at top level; we may sort but we will not further filter.
165 * This should save time for the critical top-level view by avoiding
166 * performing a redundant second check that all values are Estd values,
167 * at least with the default filter in place.
168 */
169 public static final boolean TOP_LEVEL_ESTD_FILTER = false;
170
171 /**Routine to extract cache entry from given cache; null if no cached entry.
172 * If not CACHEING then always return null,
173 * else if raw cache entry is a SoftReference then it is de-referenced,
174 * else the raw entry is returned (null if no entry).
175 */
176 private static Object _extractCacheEntry(final Map cache,
177 final Object key)
178 {
179 // If not cacheing, don't even attempt a lookup.
180 if(!CACHEING) { return(null); }
181
182 // Extract the entry if any.
183 final Object rawEntry = cache.get(key);
184
185 // If a SoftReference (can only only true if memory-sensitive cache),
186 // return de-referenced.
187 if(MEMORY_SENSITIVE_CACHE && (rawEntry instanceof SoftReference))
188 { return(((SoftReference) rawEntry).get()); }
189
190 // Return the unvarnished entry.
191 return(rawEntry);
192 }
193
194 /**Insert item in given cache, forcing non-SoftReference if need be.
195 * If is a memory-sensitive cache
196 * and strongRef is false or we've not got lots of free memory,
197 * then the item will be cached via a SoftReference,
198 * else a strong reference will be used.
199 * <p>
200 * To avoid ambiguity,
201 * the item to be cached must not itself be a SoftReference.
202 * <p>
203 * If not CACHEING, this does nothing.
204 */
205 @SuppressWarnings("unchecked")
206 private static void _insertCacheEntry(final Map cache,
207 final Object key,
208 final Object item,
209 final boolean strongRef)
210 {
211 if(!CACHEING) { return; }
212
213 if(MEMORY_SENSITIVE_CACHE && (!strongRef || !MemoryTools.lotsFree()))
214 { cache.put(key, new SoftReference(item)); }
215 else
216 { cache.put(key, item); }
217 }
218
219 /**Get selectViewLocationVirtualPrefix() value suitable for use as title; "" if none available. */
220 public CharSequence selectViewLocationVirtualPrefixAsTitle(final AEParams aeps,
221 final ServletContext context)
222 {
223 final LocationMap lm = aep.epgi.getLocationMap();
224 final Name result = selectViewLocationVirtualPrefix(aeps, lm);
225 if(Name.EMPTY.equals(result)) { return(""); }
226 return(_extractMainWordPrefixFromVirtualPrefix(result));
227 }
228
229 /**Extract main-word-prefix portion from LocationMap virtual prefix.
230 * Eg, from "x/y-z-" return "y-z-".
231 */
232 private static CharSequence _extractMainWordPrefixFromVirtualPrefix(final Name virtPrefix)
233 { return(virtPrefix.subSequence(TextUtils.indexOf(virtPrefix, '/') + 1, virtPrefix.length())); }
234
235 /**Private cache for selectViewLocationVirtualPrefix(); never null.
236 * A map from AEParams-synthesised key to (possibly SoftReference to)
237 * LocationMap lookup key for area for which title is generated.
238 */
239 private final Hashtable<String,Object> _sVLVP_cache = new Hashtable<String,Object>(103);
240
241 /**Get best-match prefix of form "section/main-words-" from LocationMap for current view, or "" if none; never null.
242 */
243 public Name selectViewLocationVirtualPrefix(final AEParams aeps,
244 final LocationMap lm)
245 {
246 // Create a lookup key (non-constrained).
247 final Object key = aeps.makeKey(false);
248
249 synchronized(_sVLVP_cache)
250 {
251 Name result = (Name) _extractCacheEntry(_sVLVP_cache, key);
252
253 // If no cache entry or a cleared one,
254 // compute and cache the filtered exhibit set.
255 if(!CACHEING || (result == null))
256 {
257 // The result value is assumed already to be intern()ed.
258 result = AEUtils.getViewLocationMapVirtualPrefix(aeps, lm);
259
260 // Cache computed prefix.
261 // Always keep top few levels which may be expensive to
262 // compute and should consume little memory.
263 _insertCacheEntry(_sVLVP_cache, key, result,
264 aeps.getZoomFactor() <= AEUtils.INITIAL_ZOOM + 3);
265 }
266
267 return(result);
268 }
269 }
270
271
272 /**Private cache for selectViewLocationVirtualPrefix(); never null.
273 * A map from AEParams-synthesised key to (possibly SoftReference to)
274 * LocationMap lookup key for area for which title is generated.
275 */
276 private final Hashtable<String,Object> _sVLL_cache = new Hashtable<String,Object>(103);
277
278 /**Create HTML label text to overlay the map; returns "" if none, never null.
279 * This creates an unnumbered list with one or more absolutely-positioned
280 * (within the encompassing relatively positioned parent element).
281 * If there are no labels to show then this does not create the UL element
282 * and the result is "".
283 * <p>
284 * The first view labels may be rendered more strongly.
285 */
286 public String selectViewLocationLabels(final DataSourceBean dsb,
287 final AEParams aeps)
288 {
289 // Create a lookup key (unconstrained).
290 final Object key = aeps.makeKey(false);
291
292 synchronized(_sVLL_cache)
293 {
294 String result = (String) _extractCacheEntry(_sVLL_cache, key);
295
296 // If no cache entry or a cleared one,
297 // compute and cache the filtered exhibit set.
298 if(!CACHEING || (result == null))
299 {
300 // Ask for overlay thumbnails when the zoom is positive
301 // ie when the map is starting to be visibly pixelated.
302 // The result value is probably worth intern()ing
303 // since this value may be slow to compute and far from unique.
304 result = MemoryTools.intern(AEUtils.getViewLocationLabels(aeps,
305 dsb,
306 this,
307 aeps.getZoomFactor() > 0));
308
309 // Cache computed prefix.
310 // Always keep top few levels
311 // which may be expensive to compute
312 // but which should consume relatively little memory.
313 _insertCacheEntry(_sVLL_cache, key, result,
314 aeps.getZoomFactor() <= AEUtils.MIN_ZOOM + 2);
315 }
316
317 return(result);
318 }
319 }
320
321
322 /**Private cache for selectContainedExhibits(); never null.
323 * A map from AEParams-synthesised key to (possibly SoftReference to)
324 * immutable String List of exhibits with centres in the current map view.
325 * <p>
326 * All access is synchronised on this object.
327 * <p>
328 * A Hashtable is used as inherently thread-safe.
329 * <p>
330 * Start this small; let it grow if needed.
331 */
332 private final Hashtable<String,Object> _sCE_cache = new Hashtable<String,Object>(137);
333
334 /**Selects an immutable List of exhibits whose centres lie in the current map view; never null.
335 * If we are using tile encoding,
336 * then this caches its results via a SoftReference
337 * based on the constrained/canonicalised AEParams value
338 * set from the current request.
339 * The cache is cleared when the exhibit set changes.
340 * The very top result or results at the outermost zooms
341 * (that a typical visitor is likely to encounter first)
342 * may be cached with strong references to ensure that they
343 * remain in cache for fast access.
344 * <p>
345 * If we are not using a tile encoding then this does not cache at all,
346 * but just generates everything on demand.
347 * <p>
348 * This routine tries to avoid drawing each distinct point on the map
349 * more than once to save time on the assumptions that:
350 * <ul>
351 * <li>The marker is idempotent, so redrawing does not add anything.
352 * <li>Drawing is relatively expensive.
353 * </ul>
354 *
355 * @param aeps parameters with which map to display is chosen; not null
356 * @return immutable List of full names of exhibits in current map view; not null
357 */
358 public List<Name.ExhibitFull> selectContainedExhibits(final AEParams aeps)
359 {
360 // Create a lookup key.
361 // This can be coarse-grained since it depends only on the viewport.
362 final Object key = aeps.makeKey(true);
363
364 synchronized(_sCE_cache)
365 {
366 List<Name.ExhibitFull> result = (List<Name.ExhibitFull>) _extractCacheEntry(_sCE_cache, key);
367
368 // If no cache entry or a cleared one
369 // then compute and cache the filtered exhibit set.
370 if(!CACHEING || (result == null))
371 {
372 // Get String[] set of exhibits to filter.
373 // Nominally the entire set filtered by the base filter bean.
374 final Name.ExhibitFull[] inputNames =
375 _getRawInputSetForFilter(aeps);
376
377 // Filter for exhibits in the current map bounds.
378 final Expr expr = _chooseLocationFilter(aeps);
379 final Name.ExhibitFull[] rawResult = (expr == null) ? inputNames :
380 expr.eval(aep, inputNames);
381 // ...sort into human-friendly order...
382 Arrays.sort(rawResult, ExhibitAttrUtils.getAttrWords().SMART_ORDER);
383 // ...and prepare to cache an unmodifiable (and thread-safe) copy.
384 // Eliminate duplicate empty lists (use single instance).
385 result = (rawResult.length == 0) ?
386 NO_EXHIBITS :
387 Collections.unmodifiableList(Arrays.asList(rawResult));
388
389 // Cache new List if appropriate.
390 // Always keep top couple of levels which are expensive to
391 // compute and should basically share a few Strings.
392 _insertCacheEntry(_sCE_cache, key, result,
393 aeps.getZoomFactor() <= AEUtils.INITIAL_ZOOM + 1);
394 }
395
396 return(result);
397 }
398 }
399
400 /**Returns the String[] set of full exhibit names to filter; never null.
401 * This is nominally the entire set filtered by the base filter bean,
402 * but may instead be a subset thereof from an area enclosing the
403 * current view, eg from a zoomed-out view.
404 *
405 * @param aeps parameters for current view of map; never null
406 */
407 private Name.ExhibitFull[] _getRawInputSetForFilter(final AEParams aeps)
408 {
409 //System.out.println("[_getRawInputSetForFilter("+aeps+")..]");
410
411 List<Name.ExhibitFull> inputSet = null;
412
413 // Attempt to optimise by filtering from a smaller (super) set,
414 // trying successively-more zoomed-out views,
415 // though don't bother to use top-most, assumed-full, set.
416 // If not possible, revert to filtering from entire top-level set.
417 if(RECURSIVE_SELECT)
418 {
419 for(int zozf = aeps.getZoomFactor(); --zozf > AEUtils.MIN_ZOOM; )
420 {
421 final AEParams zoomedOut = aeps.makeClone().setZoomFactor(zozf);
422 // Use of strictlyContainsArea() ensures:
423 // * The zoomed-out view is a superset of our current view.
424 // * That the recursion will terminate.
425 final boolean canUseZoomedOutSuperset =
426 zoomedOut.getLocation(true).strictlyContainsArea(aeps.getLocation(true));
427 //System.out.println(" [_getRawInputSetForFilter("+aeps+"): able to use zoomed-out set: "+canUseZoomedOutSuperset+" at "+zoomedOut+".]");
428 if(canUseZoomedOutSuperset)
429 {
430 inputSet = selectContainedExhibits(zoomedOut);
431 break; // OK, can stop looking.
432 }
433 }
434 }
435
436 // If we dind't find a suitable sub-set to filter from
437 // then use the full set.
438 if(inputSet == null) { inputSet = allEstdExhibits; }
439
440 final Name.ExhibitFull[] inputNames = new Name.ExhibitFull[inputSet.size()];
441 inputSet.toArray(inputNames);
442
443 //System.out.println("[_getRawInputSetForFilter() returned length = "+inputNames.length+"]");
444 return(inputNames);
445 }
446
447 /**Choose the filter to apply to the source data set of null if not filter is to be applied.
448 * This may return null on the outermost zoom level to return all the
449 * input data, else it will return a filter to select exhibits
450 * within the current view area only.
451 *
452 * @param aeps current properties (zoom, etc); never null
453 * @return filter expression
454 */
455 private static FilterExpr _chooseLocationFilter(final AEParams aeps)
456 {
457 if(!TOP_LEVEL_ESTD_FILTER &&
458 (aeps.getZoomFactor() == AEUtils.MIN_ZOOM))
459 { return(null); }
460
461 return(new FilterExpr(null,
462 new BuiltInFilters.filtByEstdLocationCentre(aeps.getLocation(true))));
463 }
464
465
466 /**Private cache for selectMapEncodedImage(); never null.
467 * A map from AEParams-synthesised key to (possibly SoftReference to)
468 * byte[] image.
469 * <p>
470 * All access is synchronised on this object.
471 * <p>
472 * This can be clear()ed by clearCache().
473 * <p>
474 * A Hashtable is used as inherently thread-safe.
475 * <p>
476 * Start this small; let it grow if needed.
477 */
478 private final Hashtable<String,Object> _sMEI_cache = new Hashtable<String,Object>(119);
479
480 /**Create an encoded map fragment for display; never null.
481 * If we are using tile encoding,
482 * then this caches its images via a SoftReference
483 * based on the constrained/canonicalised AEParams value
484 * set from the current request.
485 * The cache is cleared when the exhibit set changes.
486 * The very top result or results at the outermost zooms
487 * (that a typical visitor is likely to encounter first)
488 * may be cached with strong references to ensure that they
489 * remain in cache for fast access.
490 * <p>
491 * If we are not using a tile encoding then this does not cache at all,
492 * but just generates everything on demand.
493 * <p>
494 * Access is synchronized (on a private lock) so as to:
495 * <ul>
496 * <li>Protect the cache if one is used.
497 * <li>Limit peak resource consumption (CPU and memory).
498 * </ul>
499 * However, a different lock may be held while a new image is being
500 * created to the normal cache-access lock in order that the creation
501 * of new images does not block retrieval of already-generated ones.
502 * <p>
503 * Draws a (red) marker cross at the centre of every exhibit
504 * within the current area,
505 * and zero-or-more titles/labels to highlight the selected area.
506 *
507 * @param aeps parameters with which map to display is chosen; not null
508 */
509 public ROByteArray selectMapEncodedImage(final AEParams aeps,
510 final ServletContext context)
511 throws IOException
512 {
513 // Create a lookup key.
514 // Treat minimum zoom specially to get a single unique image
515 // for memory efficiency and speed.
516 final boolean zoomedOut = (aeps.getZoomFactor() == AEUtils.MIN_ZOOM);
517 final Object key = aeps.makeKey(zoomedOut);
518
519 synchronized(_sMEI_cache)
520 {
521 ROByteArray result = (ROByteArray) _extractCacheEntry(_sMEI_cache, key);
522
523 // If no cache entry or a cleared one,
524 // compute and cache the image.
525 if(!CACHEING || (result == null))
526 {
527 //System.out.println("Making map fragment for: " + aeps);
528
529 // Get a raw zoomed fragment of the underlying map...
530 final BufferedImage bi = AEUtils.makeScaledBaseMapFragment(
531 aeps.getSourceRectangleToDisplay(),
532 context);
533
534 // Get the set of exhibits in this map area.
535 final List<Name.ExhibitFull> exhibits = selectContainedExhibits(aeps);
536
537 // For each exhibit, compute its centre on this fragment,
538 // and mark it on the image.
539 _drawExhibitMarkers(exhibits, aep, aeps, bi);
540
541 // Draw on the title marker, if any.
542 // None at the outermost zoom.
543 if(!zoomedOut)
544 { _drawTitleMarkers(aep.epgi.getLocationMap(), aeps, bi); }
545
546 // Force to an indexed image with small colour palette
547 // for on-the-wire efficiency.
548 final BufferedImage crbi =
549 ImageUtils.makeColourReducedBufferedImage(bi,
550 32,
551 true);
552
553 // Finally create the (compact) encoded binary form.
554 // We throw away anything that is a duplicate,
555 // since most views of one tile will be identical for example.
556 result = MemoryTools.intern(
557 new ROByteArray(AEUtils.ETP.handler.makeImageBinary(crbi, Integer.MAX_VALUE)));
558
559 // Cache new image if appropiate.
560 // Always keep the initial zoom so user's first view is fast.
561 _insertCacheEntry(_sMEI_cache, key, result, zoomedOut);
562 }
563
564 return(result);
565 }
566 }
567
568 /**Draw the marker(s) corresponding to the title location, if any.
569 * If there is no title then this may draw nothing.
570 * <p>
571 * This may draw crosshairs for the centroid.
572 */
573 private void _drawTitleMarkers(final LocationMap lm,
574 final AEParams aeps,
575 final BufferedImage bi)
576 {
577 assert(lm != null);
578 final CharSequence prefix = selectViewLocationVirtualPrefix(aeps, lm);
579 if(prefix.length() == 0) { return; } // Nothing to draw.
580
581 if(IsDebug.isDebug) { System.out.println("[_drawTitleMarkers(): using prefix="+prefix+".]"); }
582
583
584 // TODO: Draw crosshairs for unconstrained view centroid.
585 final Location.Estd viewAreaUnconstrained = aeps.getLocation(false);
586 final Point centroid =
587 aeps.getDisplayPixelForEstdLocationCentre(viewAreaUnconstrained);
588 if(centroid != null)
589 {
590 final int cx = centroid.x;
591 final int cy = centroid.y;
592 _drawVLine(bi, AEUtils.EXHIBIT_MARKER_RGB_COLOUR, cx, 0, AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT-1);
593 _drawHLine(bi, AEUtils.EXHIBIT_MARKER_RGB_COLOUR, cy, 0, AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH-1);
594 }
595
596
597 // Get area used for the title;
598 // if null, nothing to draw.
599 final Location.Estd titleArea = (Location.Estd)
600 lm.getMapFromNamePrefixToLocation().get(prefix);
601 if(titleArea == null) { return; }
602
603 final Point p =
604 aeps.getDisplayPixelForEstdLocationCentre(titleArea);
605 if(p != null)
606 { bi.setRGB(p.x, p.y, AEUtils.TITLE_AREA_MARKER_RGB_COLOUR); }
607
608 final float ev = titleArea.getE().value.floatValue();
609 final float ee = titleArea.getE().error.floatValue();
610 final int minX = aeps.computeXfromE(ev - ee);
611 final int maxX = aeps.computeXfromE(ev + ee);
612
613 final float nv = titleArea.getN().value.floatValue();
614 final float ne = titleArea.getN().error.floatValue();
615 final int minY = aeps.computeYfromN(nv + ne);
616 final int maxY = aeps.computeYfromN(nv - ne);
617
618 _drawVLine(bi, AEUtils.TITLE_AREA_MARKER_RGB_COLOUR, minX, minY-TITLE_AREA_EG, maxY+TITLE_AREA_EG);
619 _drawVLine(bi, AEUtils.TITLE_AREA_MARKER_RGB_COLOUR, maxX, minY-TITLE_AREA_EG, maxY+TITLE_AREA_EG);
620
621 _drawHLine(bi, AEUtils.TITLE_AREA_MARKER_RGB_COLOUR, minY, minX-TITLE_AREA_EG, maxX+TITLE_AREA_EG);
622 _drawHLine(bi, AEUtils.TITLE_AREA_MARKER_RGB_COLOUR, maxY, minX-TITLE_AREA_EG, maxX+TITLE_AREA_EG);
623 }
624
625 /**Number of extra pixels to draw the marker in each direction; non-negative. */
626 private static final int MARKER_EXTRA_PIXELS = 2;
627
628 /**Pixel step for title area boundaries, 1 means solid; strictly positive. */
629 private static final int TITLE_AREA_BOUNDARY_STEP = 2;
630
631 /**Extra size of corner guides on marker title area in pixels, larger than exhibit marker for emphasis; strictly positive. */
632 private static final int TITLE_AREA_EG = 2 * (MARKER_EXTRA_PIXELS + TITLE_AREA_BOUNDARY_STEP);
633
634 /**Draw vertical line of given colour at x from minY to maxY.
635 * This is careful not to attempt to draw pixels out of the image.
636 */
637 private static void _drawVLine(final BufferedImage bi,
638 final int colour,
639 final int x,
640 int minY,
641 int maxY)
642 {
643 if(minY > maxY)
644 { throw new IllegalArgumentException(); }
645
646 if((x < 0) || (x >= bi.getWidth()))
647 { return; } // Entirely off the visible image; nothing to do.
648
649 if(minY < 0) { minY = 0; }
650 if(maxY >= bi.getHeight()) { maxY = bi.getHeight() - 1; }
651
652 for(int y = minY; y <= maxY; y += TITLE_AREA_BOUNDARY_STEP)
653 { bi.setRGB(x, y, colour); }
654 }
655
656 /**Draw horizontal line of given colour at y from minX to maxX.
657 * This is careful not to attempt to draw pixels out of the image.
658 */
659 private static void _drawHLine(final BufferedImage bi,
660 final int colour,
661 final int y,
662 int minX,
663 int maxX)
664 {
665 if(minX > maxX)
666 { throw new IllegalArgumentException(); }
667
668 if((y < 0) || (y >= bi.getHeight()))
669 { return; } // Entirely off the visible image; nothing to do.
670
671 if(minX < 0) { minX = 0; }
672 if(maxX >= bi.getWidth()) { maxX = bi.getWidth() - 1; }
673
674 for(int x = minX; x <= maxX; x += TITLE_AREA_BOUNDARY_STEP)
675 { bi.setRGB(x, y, colour); }
676 }
677
678 /**Draw the markers for individual exhibits.
679 *
680 * @param exhibits List of exhibits; never null
681 * @param aep exhibit properties; nevr null
682 * @param aeps current view; never null
683 * @param bi BufferedImage to draw markers on; never null
684 */
685 static private void _drawExhibitMarkers(final List<Name.ExhibitFull> exhibits,
686 final AllExhibitProperties aep,
687 final AEParams aeps,
688 final BufferedImage bi)
689 {
690 /**Keep a collection of points already drawn to avoid redrawing them redundantly. */
691 final HashSet<Point> pointsDrawn = new HashSet<Point>(exhibits.size() | 1);
692 // Work through all the putative exhibits.
693 for(final Iterator<Name.ExhibitFull> it = exhibits.iterator(); it.hasNext(); )
694 {
695 final Name.ExhibitFull exhibitName = it.next();
696 final Location.Base lb = aep.getLocation(exhibitName);
697 if(!(lb instanceof Location.Estd)) { continue; }
698 final Location.Estd le = (Location.Estd) lb;
699
700 final Point p =
701 aeps.getDisplayPixelForEstdLocationCentre(le);
702 //System.out.println("Exhibit marker at "+p+" for exhibit " + exhibitName);
703
704 // Don't draw if already drawn.
705 if(!pointsDrawn.add(p))
706 {
707 //System.out.println(" [Avoided drawing redundant exhibit marker at "+p+" for exhibit " + exhibitName + " (" + pointsDrawn.size() + '/' + exhibits.size() + ").]");
708 continue;
709 }
710
711 // Attempt to absorb (but report) any unexpected snafus.
712 try {
713 // Draw marker for exhibit.
714 _drawExhibitMarker(p, bi);
715 }
716 catch(final Exception e)
717 {
718 System.err.println("Problem drawing map for " + aeps);
719 System.err.println("Target point " + p);
720 System.err.println("Display W,H = " + AEUtils.DISPLAY_2D_EARTH_MAP_WIDTH + "," + AEUtils.DISPLAY_2D_EARTH_MAP_HEIGHT);
721 System.err.println("Actual image W,H = " + bi.getWidth() + "," + bi.getHeight());
722 e.printStackTrace(); //To change body of catch statement use Options | File Templates.
723 }
724 }
725 }
726
727 /**Draw marker on map image fragment to indicate location of an exhibit.
728 * The point argument is itself guaranteed to be within the supplied
729 * image area, but not all pixels adjacent to it are, ie it might be
730 * on an edge.
731 *
732 * @param p location to centre the exhibit market
733 * @param bi image to draw the marker on
734 */
735 private static void _drawExhibitMarker(final Point p, final BufferedImage bi)
736 {
737 if(p != null)
738 {
739 // Centre pixel is always within the image bounds.
740 bi.setRGB(p.x, p.y, AEUtils.EXHIBIT_MARKER_RGB_COLOUR);
741 //System.out.println("Drawn exhibit marker centre at ("+p.x+","+p.y+").");
742
743 // If not just drawing a point glyph, draw it on.
744 if(MARKER_EXTRA_PIXELS > 0)
745 {
746 // Draw crosshairs.
747 for(int yoff = MARKER_EXTRA_PIXELS + 1; --yoff > 0; )
748 {
749 final int yn = p.y - yoff;
750 if(yn >= 0) { bi.setRGB(p.x, yn, AEUtils.EXHIBIT_MARKER_RGB_COLOUR); }
751 final int yp = p.y + yoff;
752 if(yp < bi.getHeight()) { bi.setRGB(p.x, yp, AEUtils.EXHIBIT_MARKER_RGB_COLOUR); }
753 }
754 for(int xoff = MARKER_EXTRA_PIXELS + 1; --xoff > 0; )
755 {
756 final int xn = p.x - xoff;
757 if(xn >= 0) { bi.setRGB(xn, p.y, AEUtils.EXHIBIT_MARKER_RGB_COLOUR); }
758 final int xp = p.x + xoff;
759 if(xp < bi.getWidth()) { bi.setRGB(xp, p.y, AEUtils.EXHIBIT_MARKER_RGB_COLOUR); }
760 }
761 }
762 }
763 }
764 }