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        }