001    /*
002    Copyright (c) 1996-2012, 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.svrCore.location;
031    
032    import java.io.BufferedInputStream;
033    import java.io.File;
034    import java.io.FileInputStream;
035    import java.io.IOException;
036    import java.io.InputStream;
037    import java.io.Serializable;
038    import java.util.Properties;
039    
040    import org.hd.d.pg2k.svrCore.MemoryTools;
041    import org.hd.d.pg2k.svrCore.PGException;
042    
043    /**Classes to hold (time and space) location data on an item.
044     * Such location data is immutable.
045     * <p>
046     * This is not considered an attribute of an item in the Gallery,
047     * but instead data associated with such an item.  Many may not
048     * have this data at all.
049     * <p>
050     * Location data is read from a Properties map using
051     * Location.Base.buildFromProperties() to construct the
052     * appropriate subtype of Base to contain the data (or
053     * throw an exception if there is one).  See the routine's
054     * description for the properties used.
055     * <p>
056     * Note that while location data read for a specific exhibit
057     * is taken to be `specific' else not, this can be overridden
058     * by use of the `specific' flag which takes a boolean ``true''
059     * or ``false'' value.
060     * <p>
061     * TODO: needs object verification after deserialisation
062     *     and mutable values need to be cloned.
063     *
064     * @author Damon Hart-Davis
065     * @version 1.41 01/08/14
066     */
067    public final class Location
068        {
069        /**Don't construct instances of this container class. */
070        private Location() { }
071    
072        /**Base class of location value with error bounds.
073         * The value is the mid-point of range of the value,
074         * and the error is the width of the interval, ON EITHER SIDE OF
075         * the value (the mid-point), encompassing both any actual `duration'
076         * or `size' of the item and any error or uncertainty in
077         * the value.  Thus the value can be shown as +/- the error.
078         * <p>
079         * The error values returned must always be non-negative
080         * (zero is allowed).
081         * <p>
082         * This abstract class can be derived to store the units
083         * in various forms.  All derived classes must be immutable,
084         * so we will only allow it to be constructed by well-behaved
085         * sub-classes that use guaranteed-immutable Number args,
086         * and that also ensure that the error value is non-negative.
087         * <p>
088         * This and any deriving classes should be immutable,
089         * serialisable, and support equals() and hashCode().
090         * <p>
091         * TODO: should validate on deserialisation.
092         */
093        public static abstract class ValueAndBounds implements Serializable
094            {
095            /**Construct a new value.
096             * The constructor visibility limits deriving classes to this package.
097             * This helps ensure that we can guarantee features sush as
098             * immutability.
099             */
100            ValueAndBounds(final Number v, final Number e)
101                {
102                if((v == null) || (e == null))
103                    { throw new IllegalArgumentException("null arg"); }
104                value = v; error = e;
105                }
106    
107            /**Central value; never null. */
108            public final Number value;
109            /**Error bounds; never null. */
110            public final Number error;
111    
112            /**Currently depends on only value for speed. */
113            @Override
114            public int hashCode()
115                { return(value.hashCode()); }
116    
117            /**Depends on equality of value and error. */
118            @Override
119            public boolean equals(final Object obj)
120                {
121                if(!(obj instanceof ValueAndBounds)) { return(false); }
122                final ValueAndBounds other = (ValueAndBounds) obj;
123                return((value.equals(other.value)) &&
124                       (error.equals(other.error)));
125                }
126    
127            /**Unique Serialisation class ID generated by http://random.hd.org/. */
128            private static final long serialVersionUID = 1802455018686196426L;
129            }
130    
131        /**Value-and-error stored as longs, eg for time.
132         * TODO: should do validation on deserialisation.
133         */
134        public static final class LongValueAndBounds extends ValueAndBounds
135            {
136            /**Make new bounded value. */
137            public LongValueAndBounds(final long _val, final long _err)
138                {
139                super(new Long(_val), new Long(_err));
140                if(_err < 0)
141                    { throw new IllegalArgumentException("-ve error"); }
142                }
143            /**Make new bounded value. */
144            public LongValueAndBounds(final Long _val, final Long _err)
145                {
146                super(_val, _err);
147                if(_err.longValue() < 0)
148                    { throw new IllegalArgumentException("-ve error"); }
149                }
150    
151            /**Unique Serialisation class ID generated by http://random.hd.org/. */
152            private static final long serialVersionUID = 4934329592740791248L;
153            }
154    
155        /**Value-and-error stored as doubles, eg for place.
156         * TODO: should do validation on deserialisation.
157         */
158        public static final class DoubleValueAndBounds extends ValueAndBounds
159            {
160            /**Make new bounded value. */
161            public DoubleValueAndBounds(final double _val, final double _err)
162                {
163                super(new Double(_val), new Double(_err));
164                if(_err < 0)
165                    { throw new IllegalArgumentException("-ve error"); }
166                }
167            /**Make new bounded value. */
168            public DoubleValueAndBounds(final Double _val, final Double _err)
169                {
170                super(_val, _err);
171                if(_err.doubleValue() < 0)
172                    { throw new IllegalArgumentException("-ve error"); }
173                }
174    
175            /**Unique Serialisation class ID generated by http://random.hd.org/. */
176            private static final long serialVersionUID = -666396322028089442L;
177            }
178    
179        /**Base of all location classes.
180         * All such classes contain an optional time value
181         * (with error bounds), and flag to indicate if this
182         * location information was specific to this collection
183         * item, or applied to a group of items.
184         * <p>
185         * A true value for `specific' says that the item had
186         * its own time info rather than inheriting a general value.
187         * <p>
188         * If the time value is zero it indicates that none is known.
189         * (If the time value is zero, so should its error bound be.)
190         * <p>
191         * All classes derived from this should be immutable
192         * and serialisable and support equals() and hashCode().
193         * <p>
194         * TODO: should do validation on deserialisation.
195         */
196        public static abstract class Base implements java.io.Serializable, MemoryTools.Internable
197            {
198            /**Build new base from properties.
199             * Looks for the type field, which must be present,
200             * and the optional time fields.
201             * <p>
202             * Note that the _specific parameter is overridden
203             * where a `specific' property is present.
204             * <p>
205             * Package-visible to restrict the range of derived classes.
206             *
207             * @param _specific  is true if the location info was specifically
208             *                   for this item, else false to indicate that
209             *                   this location info was inherited from some
210             *                   more general source
211             */
212            protected Base(final boolean _specific,
213                           final String prefix,
214                           final Properties p)
215                throws PGException
216                {
217                type = p.getProperty(prefix + typeKey);
218                if(type == null)
219                    { throw new PGException("missing type field: " + prefix + typeKey); }
220                final Boolean pSpec = getBooleanProp(prefix, p, specificKey, false);
221                specific = ((pSpec == null) ? _specific : pSpec.booleanValue());
222                }
223            /**The type of this location info. */
224            public final String type;
225            /**Did this item have its own specific location info? */
226            public final boolean specific;
227    
228            /**Hash depends on type and "specific" flag. */
229            @Override
230            public int hashCode()
231                { return(type.hashCode() ^ (specific ? 0 : 5)); }
232    
233            /**Hash depends on equality of type and "specific" flags.
234             * A derived class should probably make use of this recursively.
235             */
236            @Override
237            public boolean equals(final Object obj)
238                {
239                if(obj == this) { return(true); }
240                if(!(obj instanceof Base)) { return(false); }
241                final Base other = (Base) obj;
242                return((specific == other.specific) &&
243                       (type.equals(other.type)));
244                }
245    
246            /**Generate terse, human readable portion of location info.
247             * This should be overridden by derived classes, but
248             * with this used (via super.toString()) to generate the
249             * prefix for the result.
250             * <p>
251             * The string generated by this routine contains no
252             * white space to make the result handlable as one token
253             * and is shell- and HTML- friendly, with portions separated
254             * by colons, and deriving classes must abide by this
255             * too.
256             * <p>
257             * This generates a string consisting of the type name
258             * followed by a colon then `s' for a specific location or
259             * `g' for a generic one.
260             */
261            @Override
262            public String toString()
263                { return(type + ':' + (specific ? 's' : 'g')); }
264    
265    
266            /**Factory method to build a class from a property set; never returns null.
267             * The Properties map passed is only read (not updated),
268             * and consists of a number of keys prefixed with
269             * the given prefix (either the empty string, or
270             * a fully-qualified name prefix, ending with ".").
271             * <p>
272             * Whatever type of map is being built, there must
273             * always be the key given by the prefix + typeKey,
274             * which must contain the type of location info, and
275             * that type must be the name of the class in this
276             * Location outer class, derived from Base, that holds
277             * the location info.
278             * <p>
279             * For robustness, if we cannot use the location info
280             * for some reason, we should probably pretend that the
281             * item has none, though flag the problem to stderr.
282             *
283             * @exception PGException in case of inability to
284             *     build a location value from the specified
285             *     properties
286             */
287            public static Base buildFromProperties(final boolean specific,
288                                                   final String prefix,
289                                                   final Properties p)
290                throws PGException
291                {
292                // Find the type.
293                final String type = p.getProperty(prefix + typeKey);
294                if(type == null)
295                    { throw new PGException("missing type field: " + prefix + typeKey); }
296    
297                // Try to find the class indicated by the type...
298                // Do this by hand rather than by pure reflection.
299                Class<? extends Base> c;
300                if("Base".equals(type)) { c = Base.class; }
301                else if("None".equals(type)) { c = None.class; }
302                else if("Time".equals(type)) { c = Time.class; }
303                else if("Estd".equals(type)) { c = Estd.class; }
304                else
305                    { throw new PGException("unknown location type (class not found): " + type); }
306    
307                // Try to find the constructor we will need...
308                final Class<?> argTypes[] =
309                    { boolean.class, String.class, Properties.class };
310                java.lang.reflect.Constructor<? extends Base> cons;
311                try { cons = c.getConstructor(argTypes); }
312                catch(final NoSuchMethodException e)
313                    { throw new PGException("unsuitable location type (no such method): " + type); }
314                if(cons == null)
315                    { throw new PGException("unsuitable location type (no constructor): " + type); }
316    
317                // Now try and make an instance...
318                // We try to instance-control the values we create to help conserve memory.
319                final Object args[] =
320                    { specific ? Boolean.TRUE : Boolean.FALSE, prefix, p };
321                final Object o;
322                try { o = cons.newInstance(args); }
323                catch(final ThreadDeath t) { throw t; } // Die on command.
324                catch(final Throwable t)
325                    {
326                    t.printStackTrace();
327                    throw new PGException("uninstantiable location type (unexpected exception): " + type + ": " + t.getMessage());
328                    }
329                if(!(o instanceof Base))
330                    { throw new PGException("unknown location type (bad class): " + type); }
331    
332                return(MemoryTools.intern((Base) o)); // All done!
333                }
334            /**Build from properties with empty prefix. */
335            public static Base buildFromProperties(final boolean specific, final Properties p)
336                throws PGException
337                { return(buildFromProperties(specific, "", p)); }
338            /**Build directly from a file, assuming empty prefix. */
339            public static Base buildFromFile(final boolean specific, final File f)
340                throws IOException, PGException
341                {
342                final InputStream is = new BufferedInputStream(new FileInputStream(f));
343    //            if(is == null) { throw new IOException("could not open location properties file: " + f); }
344                try {
345                    final Properties p = new Properties();
346                    p.load(is);
347                    return(buildFromProperties(specific, p));
348                    }
349                finally { is.close(); }
350                }
351            /**Attempts to build location from file.
352             * Returns Location.NONE type in case of difficulty.
353             */
354            public static Base buildFromFileOrNONE(final boolean specific, final File f)
355                {
356                if(!f.exists()) { return(Location.NONE); }
357                try {
358                    final Base result = buildFromFile(specific, f);
359    //System.err.println(" " + f + ": OK");
360                    return(result);
361                    }
362                catch(final PGException e)
363                    {
364    System.err.println(" " + f + ": pretending no location info: " + e);
365                    return(Location.NONE);
366                    }
367                catch(final IOException e)
368                    {
369    System.err.println(" " + f + ": IO problem: pretending no location info: " + e);
370                    return(Location.NONE);
371                    }
372                }
373    
374            /**Name of key giving location type in Properties map. */
375            public static final String typeKey = "type";
376    
377            /**Name of key giving specificity in Properties map. */
378            public static final String specificKey = "specific";
379    
380    
381            /**Routine to get a double property.
382             * Throws a PGException if there is a problem.
383             * <p>
384             * Can be asked to veto negative values, eg for error margins.
385             */
386            public static final Double getDoubleProp(final String prefix, final Properties p,
387                                                     final String prop, final boolean rejectNegative)
388                throws PGException
389                {
390                final String v = p.getProperty(prefix + prop);
391                if(v == null) { throw new PGException("missing property " + prop); }
392                try {
393                    final Double result = new Double(v);
394                    if(!rejectNegative) { return(result); }
395                    if(result.doubleValue() < 0)
396                        { throw new PGException("illegal negative value for property: " + prop); }
397                    return(result);
398                    }
399                catch(final NumberFormatException e)
400                    { throw new PGException("unparsable (double) property: " + prop); }
401                }
402            /**Get pair of double properties; requested one and its error interval.
403             * If the specified property is <i>X</i>, the main property is
404             * <i>X</i>Err.
405             * <p>
406             * If mandatory and either property is missing, a PGException is
407             * thrown.
408             * <p>
409             * If optional and the first property is missing, null is returned;
410             * but if the first property is present and the second is absent
411             * then a PGException is thrown.
412             */
413            public static final DoubleValueAndBounds getDoubleProps(final String prefix, final Properties p,
414                                                                    final String prop,
415                                                                    final boolean mandatory)
416                throws PGException
417                {
418                if(!mandatory && (p.getProperty(prefix + prop) == null)) { return(null); }
419                return(new DoubleValueAndBounds(
420                    getDoubleProp(prefix, p, prop, false),
421                    getDoubleProp(prefix, p, prop + "Err", true)));
422                }
423    
424    
425            /**Routine to get a boolean property.
426             * Throws a PGException if there is a problem.
427             */
428            public static final Boolean getBooleanProp(final String prefix, final Properties p,
429                                                 final String prop, final boolean mandatory)
430                throws PGException
431                {
432                final String v = p.getProperty(prefix + prop);
433                if(v == null)
434                    {
435                    if(!mandatory) { return(null); }
436                    throw new PGException("missing property " + prop);
437                    }
438                final Boolean result = Boolean.valueOf(v);
439                return(result);
440                }
441    
442    
443            /**Routine to get a double property.
444             * Throws a PGException if there is a problem.
445             * <p>
446             * Can be asked to veto negative values, eg for error margins.
447             */
448            public static final Long getLongProp(final String prefix, final Properties p,
449                                                 final String prop, final boolean rejectNegative)
450                throws PGException
451                {
452                final String v = p.getProperty(prefix + prop);
453                if(v == null) { throw new PGException("missing property " + prop); }
454                try {
455                    final Long result = new Long(v);
456                    if(!rejectNegative) { return(result); }
457                    if(result.longValue() < 0)
458                        { throw new PGException("illegal negative value for property: " + prop); }
459                    return(result);
460                    }
461                catch(final NumberFormatException e)
462                    { throw new PGException("unparsable (long) property: " + prop); }
463                }
464    
465            /**Get pair of long properties; requested one and its error interval.
466             * If the specified property is <i>X</i>, the main property is
467             * <i>X</i>Err.
468             * <p>
469             * If mandatory and either property is missing, a PGException is
470             * thrown.
471             * <p>
472             * If optional and the first property is missing, null is returned;
473             * but if the first property is present and the second absent a
474             * PGException is thrown.
475             */
476            public static final LongValueAndBounds getLongProps(final String prefix, final Properties p,
477                                                                final String prop,
478                                                                final boolean mandatory)
479                throws PGException
480                {
481                if(!mandatory && (p.getProperty(prefix + prop) == null)) { return(null); }
482                return(new LongValueAndBounds(
483                    getLongProp(prefix, p, prop, false),
484                    getLongProp(prefix, p, prop + "Err", true)));
485                }
486    
487    
488            /**Unique Serialisation class ID generated by http://random.hd.org/. */
489            private static final long serialVersionUID = -7784138110080680826L;
490            }
491    
492        /**Singleton: represents an absence of location information. */
493        private static final class None extends Base
494            {
495            /**Special way to construct distinguished NONE value. */
496            private None(final Properties p)
497                throws PGException
498                { super(false, "", p); }
499    
500            /**Make distinguished NONE value; only call this once. */
501            static None makeNone()
502                {
503                try {
504                    final Properties p = new Properties();
505                    p.put(typeKey, "None"); // Sneakily add a property...
506                    return(new None(p));
507                    }
508                catch(final PGException e)
509                    { throw new RuntimeException(e.getMessage()); }
510                }
511    
512            /**Ensure that there is at most one instance of NONE. */
513            private Object readResolve()
514                { return(NONE); }
515    
516            /**Unique Serialisation class ID generated by http://random.hd.org/. */
517            private static final long serialVersionUID = 1900944139123394635L;
518            }
519    
520        /**Single distinguished value meaning `no location info'. */
521        public static final Base NONE = None.makeNone();
522    
523        /**Just captures time; designed to be derived from.
524         * TODO: needs to validate on deserialisation.
525         */
526        public static class Time extends Location.Base
527            {
528            public Time(final boolean _specific,
529                           final String prefix,
530                           final Properties p)
531                throws PGException
532                {
533                // Get the base class initialised.
534                super(_specific, prefix, p);
535    
536                // Get optional Time prop.
537                // TODO: Needs to accept YYYY[/MM[/DD [hh:[mm:[ss[.ddd]]]]]]
538                // time format (UTC) plus nn[Y|M|D|h|m|s|ms] error, as
539                // well as raw ms values for both.
540                LongValueAndBounds t = null;
541                try { t = getLongProps(prefix, p, "Time", false); }
542                catch(final PGException e)
543                    {
544    System.err.println("[WARNING: unable to parse Time field in location data; ignoring...]");
545                    }
546                Time = t;
547                }
548    
549            /**What time does the item record? */
550            private final LongValueAndBounds Time;
551            /**Get the time value; null means unknown. */
552            public final LongValueAndBounds getTime() { return(Time); }
553    
554            /**Depends on fields from the base class and the time info, if any. */
555            @Override
556            public int hashCode()
557                {
558                return((super.hashCode() * 19) +
559                    ((Time != null) ? Time.hashCode() : 1));
560                }
561    
562            /**Depends on fields from the base class and the time info, if any. */
563            @Override
564            public boolean equals(final Object obj)
565                {
566                if(!(obj instanceof Time)) { return(false); }
567                final Time other = (Time) obj;
568    
569                // Base class fields must match.
570                if(!super.equals(obj)) { return(false); }
571    
572                // Time fields, if any, must match.
573                if(Time == null) { return(other.Time == null); }
574                else { return(Time.equals(other.Time)); }
575                }
576    
577            /**Get value as a String. */
578            @Override
579            public String toString()
580                {
581                if(Time == null) { return(super.toString()); }
582                final StringBuilder sb = new StringBuilder();
583                sb.append(super.toString());
584                sb.append(':');
585                sb.append("TIME"); // Not implemented...
586                return(sb.toString());
587                }
588    
589            /**Unique Serialisation class ID generated by http://random.hd.org/. */
590            private static final long serialVersionUID = 3902949361757128661L;
591            }
592    
593        /**Earth-standard coordinates.
594         * Eastings and Northings, with optional altitude
595         * above mean sea level (in metres).
596         * <p>
597         * This class is immutable.
598         * <p>
599         * Time is handled by a base class if any.
600         * <p>
601         * TODO: needs to validate on deserialisation.
602         */
603        public static final class Estd extends Location.Time
604            {
605            /**Build Earth-standard coordinates in WGS84 space.
606             */
607            public Estd(final boolean _specific,
608                           final String prefix,
609                           final Properties p)
610                throws PGException
611                {
612                // Get the base class initialised.
613                super(_specific, prefix, p);
614    
615                // Get mandatory E/N props.
616                E = getDoubleProps(prefix, p, "E", true);
617                N = getDoubleProps(prefix, p, "N", true);
618                // Get optional Alt prop.
619                Alt = getDoubleProps(prefix, p, "Alt", false);
620                }
621    
622            /**Get maximum value for E (positive); minimum is negation of this. */
623            public static final int MAX_E = 180;
624            /**Get maximum value for N (positive); minimum is negation of this. */
625            public static final int MAX_N = 90;
626    
627            /**How many degrees East are we? */
628            private final DoubleValueAndBounds E;
629            /**Get the Easting value; never null. */
630            public final DoubleValueAndBounds getE() { return(E); }
631    
632            /**How many degrees North are we? */
633            private final DoubleValueAndBounds N;
634            /**Get the Northing value; never null. */
635            public final DoubleValueAndBounds getN() { return(N); }
636    
637            /**How many metres above mean sea level are we? */
638            private final DoubleValueAndBounds Alt;
639            /**Get the altitude in metres; null indicates not known. */
640            public final DoubleValueAndBounds getAlt() { return(Alt); }
641    
642            /**True if this Location is "bigger" than that of its argument.
643             * True if:
644             * <ul>
645             * <li>This strictly contains the area of the argument,
646             * <li>Or, this has a larger error area on the Mercator projection,
647             *     ie NErr * EErr.
648             * </ul>
649             */
650            public boolean isLargerThan(final Location.Estd other)
651                {
652                // A completely contained area is smaller.
653                if(strictlyContainsArea(other)) { return(true); }
654                return(E.error.doubleValue() * N.error.doubleValue() >
655                       other.E.error.doubleValue() * other.N.error.doubleValue());
656                }
657    
658            /**True if this Location and bounding box contains the bounding box of the argument Location.
659             * Note that this is reflexive, ie a Location contains its own bounding box.
660             */
661            public boolean containsArea(final Location.Estd other)
662                {
663                if(equals(other)) { return(true); }
664    
665                final double thisN = getN().value.doubleValue();
666                final double thisNErr = getN().error.doubleValue();
667                final double otherN = other.getN().value.doubleValue();
668                final double otherNErr = other.getN().error.doubleValue();
669    
670                // If not in bounds for the North, not contained.
671                // Structured to fail if any of the numbers is NaN.
672                if(!((otherN - otherNErr >= thisN - thisNErr) && (otherN + otherNErr <= thisN + thisNErr)))
673                    { return(false); }
674    
675                final double thisE = getE().value.doubleValue();
676                final double thisEErr = getE().error.doubleValue();
677                final double otherE = other.getE().value.doubleValue();
678                final double otherEErr = other.getE().error.doubleValue();
679    
680                // If not in bounds for the East, not contained.
681                // Structured to fail if any of the numbers is NaN.
682                if(!((otherE - otherEErr >= thisE - thisEErr) && (otherE + otherEErr <= thisE + thisEErr)))
683                    { return(false); }
684    
685                return(true); // Seems OK.
686                }
687    
688            /**True if this Location and bounding box strictly contains the bounding box of the argument Location.
689             * Note that this is not reflexive, ie a Location not not strictly contain its own bounding box,
690             * since the contained item must be smaller,
691             * or equivalently contained but not equal.
692             */
693            public boolean strictlyContainsArea(final Location.Estd other)
694                {
695                if(equals(other)) { return(false); }
696                return(containsArea(other));
697                }
698    
699            /**True if this Location and bounding box contains the centre of the argument Location.
700             * Note that this is reflexive, ie a Location contains its own centre.
701             */
702            public boolean containsCentre(final Location.Estd other)
703                {
704                final double thisN = getN().value.doubleValue();
705                final double thisNErr = getN().error.doubleValue();
706                final double otherN = other.getN().value.doubleValue();
707    
708                // If not in bounds for the North, not contained.
709                // Structured to fail if any of the numbers is NaN.
710                if(!((otherN >= thisN - thisNErr) && (otherN <= thisN + thisNErr)))
711                    { return(false); }
712    
713                final double thisE = getE().value.doubleValue();
714                final double thisEErr = getE().error.doubleValue();
715                final double otherE = other.getE().value.doubleValue();
716    
717                // If not in bounds for the East, not contained.
718                // Structured to fail if any of the numbers is NaN.
719                if(!((otherE >= thisE - thisEErr) && (otherE <= thisE + thisEErr)))
720                    { return(false); }
721    
722                return (true); // Seems OK.
723                }
724    
725    
726            /**Depends on base hash plus E and N values. */
727            @Override
728            public int hashCode()
729                {
730                return((super.hashCode() * 69069) ^
731                       (N.hashCode() * 251) ^
732                       (E.hashCode()));
733                }
734    
735            /**Depends on all the elements of this class and base classes.
736             */
737            @Override
738            public boolean equals(final Object obj)
739                {
740                if(obj == this) { return(true); }
741                if(!(obj instanceof Estd)) { return(false); }
742                final Estd other = (Estd) obj;
743    
744                // Base class fields must match.
745                if(!super.equals(obj)) { return(false); }
746    
747                // Mandatory fields must match.
748                if(!N.equals(other.N)) { return(false); }
749                if(!E.equals(other.E)) { return(false); }
750    
751                // Alt fields, if any, must match.
752                if(Alt == null) { return(other.Alt == null); }
753                else { return(Alt.equals(other.Alt)); }
754                }
755    
756            /**Get value as a String. */
757            @Override
758            public String toString()
759                {
760                final StringBuilder sb = new StringBuilder();
761                sb.append(super.toString());
762                sb.append(':');
763                if(E.value.doubleValue() < 0)
764                    { sb.append(-E.value.doubleValue()); sb.append('W'); }
765                else
766                    { sb.append( E.value.doubleValue()); sb.append('E'); }
767                sb.append(',');
768                if(N.value.doubleValue() < 0)
769                    { sb.append(-N.value.doubleValue()); sb.append('S'); }
770                else
771                    { sb.append( N.value.doubleValue()); sb.append('N'); }
772                if(Alt != null)
773                    {
774                    sb.append(',');
775                    sb.append(Alt.value.doubleValue());
776                    sb.append('m');
777                    }
778                // Put in error bounds if needed.
779                if((E.error.doubleValue() != 0) ||
780                   (N.error.doubleValue() != 0) ||
781                   ((Alt != null) && (Alt.error.doubleValue() != 0)))
782                    {
783                    sb.append("{+/-");
784                    sb.append(E.error.doubleValue());
785                    sb.append(',');
786                    sb.append(N.error.doubleValue());
787                    if(Alt != null) { sb.append(','); sb.append(Alt.error.doubleValue()); }
788                    sb.append('}');
789                    }
790                return(sb.toString());
791                }
792    
793            /**"Whole Earth" value; not null. */
794            public static final Location.Estd WHOLE_EARTH;
795            /**Initialise "Whole Earth" value. */
796            static
797                {
798                final Properties p = new Properties();
799                p.setProperty("type", "Estd");
800                p.setProperty("E", "0");
801                p.setProperty("EErr", String.valueOf(MAX_E));
802                p.setProperty("N", "0");
803                p.setProperty("NErr", String.valueOf(MAX_N));
804                try { WHOLE_EARTH = new Estd(false, "", p); }
805                catch(final PGException e) { throw new Error("INTERNAL ERROR"); }
806                }
807    
808    
809            /**Unique Serialisation class ID generated by http://random.hd.org/. */
810            private static final long serialVersionUID = 3108417206234772844L;
811            }
812        }