001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 *
032 */
033package org.openimaj.vis.world;
034
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.Iterator;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043
044import org.openimaj.content.animation.animator.ValueAnimator;
045import org.openimaj.image.MBFImage;
046import org.openimaj.image.colour.RGBColour;
047import org.openimaj.image.renderer.MBFImageRenderer;
048import org.openimaj.image.renderer.RenderHints;
049import org.openimaj.math.geometry.point.Point2d;
050import org.openimaj.math.geometry.point.Point2dImpl;
051import org.openimaj.math.geometry.shape.Shape;
052import org.openimaj.math.geometry.transforms.TransformUtilities;
053import org.openimaj.vis.AnimatedVisualisationListener;
054import org.openimaj.vis.AnimatedVisualisationProvider;
055import org.openimaj.vis.general.AxesRenderer2D;
056import org.openimaj.vis.general.ItemPlotter;
057import org.openimaj.vis.general.LabelledPointVisualisation;
058import org.openimaj.vis.general.LabelledPointVisualisation.LabelledDot;
059import org.openimaj.vis.general.XYPlotVisualisation;
060
061import Jama.Matrix;
062
063/**
064 * Draws a world map visualisation as an XY plot. The XY coordinates are scaled
065 * to be equal to longitude and latitude.
066 * 
067 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
068 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
069 * @param <T>
070 *            The type of data to plot on the image
071 * @created 11 Jun 2013
072 */
073public class WorldMap<T> extends XYPlotVisualisation<T>
074                implements AnimatedVisualisationProvider
075{
076        /** */
077        private static final long serialVersionUID = 1L;
078
079        /** The colour to use for the sea */
080        private Float[] seaColour = new Float[] { 0.4f, 0.5f, 1f, 1f };
081
082        /** The colour to outline the countries with */
083        private Float[] defaultCountryOutlineColour = new Float[] { 0f, 0f, 0f };
084
085        /** The colour to fill the land with */
086        private Float[] defaultCountryLandColour = new Float[] { 0f, 1f, 0f };
087
088        /** The colour to fill the land when highlighted */
089        private Float[] highlightCountryLandColour = new Float[] { 1f, 0f, 0f };
090
091        /** The polygons that represent the countries in the world */
092        private WorldPolygons worldPolys;
093
094        /** List of countries which should be highlighted */
095        private final Set<String> activeCountries = new HashSet<String>();
096
097        /** These are overrides for the default country highglight colour */
098        private final HashMap<String, Float[]> countryHighlightColours = new HashMap<String, Float[]>();
099
100        /** Listeners for animations */
101        private final List<AnimatedVisualisationListener> listeners = new ArrayList<AnimatedVisualisationListener>();
102
103        /** The cached world image */
104        private MBFImage cachedWorldImage = null;
105
106        /** List of colour animations in progress */
107        private final Map<String, ValueAnimator<Float[]>> colourAnimators = new HashMap<String, ValueAnimator<Float[]>>();
108
109        /** Thread for animating colours */
110        private Thread animationThread = null;
111
112        int xmin = -180;
113        int xmax = 180;
114        int ymin = -90;
115        int ymax = 90;
116
117        /**
118         * @param width
119         *            Width of the visualisation
120         * @param height
121         *            Height of the visualisation
122         * @param plotter
123         *            The plotter to plot data with
124         */
125        public WorldMap(final int width, final int height,
126                        final ItemPlotter<T, Float[], MBFImage> plotter)
127        {
128                super(width, height, plotter);
129                this.init();
130        }
131
132        /**
133         * @param width
134         *            Width of the visualisation
135         * @param height
136         *            Height of the visualisation
137         * @param plotter
138         *            The plotter to plot data with
139         * @param xmin
140         *            min x value to be plotted
141         * @param xmax
142         *            max x value to be plotted
143         * @param ymin
144         *            min y value to be plotted
145         * @param ymax
146         *            max y value to be plotted
147         */
148        public WorldMap(final int width, final int height, final ItemPlotter<T, Float[], MBFImage> plotter,
149                        int xmin, int xmax, int ymin, int ymax)
150        {
151                super(width, height, plotter);
152                this.xmin = xmin;
153                this.xmax = xmax;
154                this.ymin = ymin;
155                this.ymax = ymax;
156                this.init();
157        }
158
159        /**
160         * Initialise
161         */
162        private void init()
163        {
164                super.setAutoScaleAxes(false);
165                super.setAutoPositionXAxis(true);
166                super.axesRenderer2D.setAutoScaleAxes(false);
167
168                super.axesRenderer2D.setMinXValue(xmin);
169                super.axesRenderer2D.setMaxXValue(xmax);
170                super.axesRenderer2D.setMinYValue(ymin);
171                super.axesRenderer2D.setMaxYValue(ymax);
172                super.axesRenderer2D.setAxisPaddingLeft(50);
173                super.axesRenderer2D.setAxisPaddingBottom(50);
174                super.axesRenderer2D.setAxisPaddingRight(50);
175                super.axesRenderer2D.setAxisPaddingTop(50);
176                super.axesRenderer2D.setxMajorTickSpacing(10);
177                super.axesRenderer2D.setyMajorTickSpacing(10);
178                super.axesRenderer2D.setxMinorTickSpacing(5);
179                super.axesRenderer2D.setyMinorTickSpacing(5);
180                super.axesRenderer2D.setxLabelSpacing(90);
181                super.axesRenderer2D.setyLabelSpacing(45);
182                super.axesRenderer2D.setxAxisColour(RGBColour.WHITE);
183                super.axesRenderer2D.setyAxisColour(RGBColour.WHITE);
184                super.axesRenderer2D.setyTickLabelColour(RGBColour.WHITE);
185                super.axesRenderer2D.setxTickLabelColour(RGBColour.WHITE);
186                super.axesRenderer2D.setDrawXAxisName(false);
187                super.axesRenderer2D.setDrawYAxisName(false);
188                super.axesRenderer2D.setDrawMajorTickGrid(false);
189                super.axesRenderer2D.setDrawMinorTickGrid(false);
190                this.worldPolys = new WorldPolygons();
191                this.addAnimatedVisualisationListener(this);
192        }
193
194        /**
195         * Add a country to highlight
196         * 
197         * @param countryCode
198         *            The country code to highlight
199         */
200        public void addHighlightCountry(final String countryCode)
201        {
202                this.activeCountries.add(countryCode);
203        }
204
205        /**
206         * Add a country to highlight
207         * 
208         * @param countryCode
209         *            The country code to highlight
210         * @param colour
211         *            The colour to highlight the country
212         */
213        public void addHighlightCountry(final String countryCode, final Float[] colour)
214        {
215                this.activeCountries.add(countryCode);
216                this.countryHighlightColours.put(countryCode, colour);
217        }
218
219        /**
220         * Remove a highlighted country
221         * 
222         * @param countryCode
223         *            The country code to remove
224         */
225        public void removeHighlightCountry(final String countryCode)
226        {
227                this.activeCountries.remove(countryCode);
228                this.countryHighlightColours.remove(countryCode);
229        }
230
231        /**
232         * Fill the image with the sea's colour. Uses the member seaColour to
233         * determine this. If you want another sea texture, override this method.
234         */
235        protected void drawSea(final MBFImage img)
236        {
237                img.fill(this.seaColour);
238        }
239
240        private void drawCachedImage(final MBFImage img,
241                        final AxesRenderer2D<Float[], MBFImage> axesRenderer)
242        {
243                synchronized (axesRenderer)
244                {
245                        System.out.println("Drawing cached world image " + img.getWidth() + "x" + img.getHeight());
246                        this.cachedWorldImage = new MBFImage(img.getWidth(), img.getHeight(), 4);
247
248                        // Fill the image with the sea colour.
249                        // We'll draw the countries on top of this.
250                        this.drawSea(this.cachedWorldImage);
251
252                        // Make the image fit into the axes centred around 0,0 long/lat
253                        final Point2d mid = axesRenderer.calculatePosition(0, 0);
254                        final Point2d dateLine0 = axesRenderer.calculatePosition(180, 0);
255                        final Point2d northPole = axesRenderer.calculatePosition(0, -90);
256
257                        System.out.println("0,0 @ " + mid);
258                        System.out.println("dateLine: " + dateLine0);
259                        System.out.println("northpole: " + northPole);
260
261                        final double scaleX = (dateLine0.getX() - mid.getX()) / 180d;
262                        final double scaleY = (northPole.getY() - mid.getY()) / 90d;
263                        Matrix trans = Matrix.identity(3, 3);
264                        trans = trans.times(
265                                        TransformUtilities.scaleMatrixAboutPoint(
266                                                        scaleX, -scaleY, mid
267                                                        )
268                                        );
269
270                        // Translate to 0,0
271                        trans = trans.times(
272                                        TransformUtilities.translateMatrix(mid.getX(), mid.getY())
273                                        );
274
275                        // Now draw the countries onto the sea. We transform each of the
276                        // shapes
277                        // by the above transform matrix prior to plotting them to the
278                        // image.
279                        for (final WorldPlace wp : this.worldPolys.getShapes())
280                        {
281                                // Each place may have more than one polygon.
282                                final List<Shape> shapes = wp.getShapes();
283
284                                final MBFImageRenderer ir = this.cachedWorldImage.createRenderer(RenderHints.ANTI_ALIASED);
285
286                                // For each of the polygons... draw them to the image.
287                                for (Shape s : shapes)
288                                {
289                                        s = s.transform(trans);
290
291                                        // Fill the country with the land colour
292                                        ir.drawShapeFilled(s, this.defaultCountryLandColour);
293
294                                        // Draw the outline shape of the country
295                                        ir.drawShape(s, 1, this.defaultCountryOutlineColour);
296                                }
297                        }
298                }
299        }
300
301        /**
302         * {@inheritDoc}
303         * 
304         * @see org.openimaj.vis.general.XYPlotVisualisation#beforeAxesRender(org.openimaj.image.MBFImage,
305         *      org.openimaj.vis.general.AxesRenderer2D)
306         */
307        @Override
308        synchronized public void beforeAxesRender(final MBFImage visImage,
309                        final AxesRenderer2D<Float[], MBFImage> axesRenderer)
310        {
311                synchronized (axesRenderer)
312                {
313                        // Redraw the world if the image dimensions aren't the same.
314                        if (this.cachedWorldImage == null ||
315                                        visImage.getWidth() != this.cachedWorldImage.getWidth() ||
316                                        visImage.getHeight() != this.cachedWorldImage.getHeight())
317                                this.drawCachedImage(visImage, axesRenderer);
318
319                        // Blat the cached image
320                        System.out.println("Blitting cached world image");
321                        visImage.drawImage(this.cachedWorldImage, 0, 0);
322
323                        // Make the image fit into the axes centred around 0,0 long/lat
324                        final Point2d mid = axesRenderer.calculatePosition(0, 0);
325                        final Point2d dateLine0 = axesRenderer.calculatePosition(180, 0);
326                        final Point2d northPole = axesRenderer.calculatePosition(0, -90);
327                        final double scaleX = (dateLine0.getX() - mid.getX()) / 180d;
328                        final double scaleY = (northPole.getY() - mid.getY()) / 90d;
329                        Matrix trans = Matrix.identity(3, 3);
330                        trans = trans.times(
331                                        TransformUtilities.scaleMatrixAboutPoint(
332                                                        scaleX, -scaleY, mid
333                                                        )
334                                        );
335
336                        // Translate to 0,0
337                        trans = trans.times(
338                                        TransformUtilities.translateMatrix(mid.getX(), mid.getY())
339                                        );
340
341                        // Now draw the countries onto the sea. We transform each of the
342                        // shapes
343                        // by the above transform matrix prior to plotting them to the
344                        // image.
345                        final HashSet<String> k = new HashSet<String>(this.activeCountries);
346                        for (final String countryCode : k)
347                        {
348                                final WorldPlace wp = this.worldPolys.byCountryCode(countryCode);
349
350                                // Each place may have more than one polygon.
351                                final List<Shape> shapes = wp.getShapes();
352
353                                final MBFImageRenderer ir = visImage.createRenderer(RenderHints.ANTI_ALIASED);
354
355                                // For each of the polygons... draw them to the image.
356                                for (Shape s : shapes)
357                                {
358                                        s = s.transform(trans);
359
360                                        // Draw the country in the highlight colour
361                                        final Float[] col = this.countryHighlightColours.get(wp.getISOA2());
362                                        ir.drawShapeFilled(s, col == null ? this.highlightCountryLandColour : col);
363
364                                        // Draw the outline shape of the country
365                                        ir.drawShape(s, 1, this.defaultCountryOutlineColour);
366                                }
367                        }
368                }
369        }
370
371        /**
372         * @return the seaColour
373         */
374        public Float[] getSeaColour()
375        {
376                return this.seaColour;
377        }
378
379        /**
380         * @param seaColour
381         *            the seaColour to set
382         */
383        public void setSeaColour(final Float[] seaColour)
384        {
385                this.seaColour = seaColour;
386        }
387
388        /**
389         * @return the defaultCountryOutlineColour
390         */
391        public Float[] getDefaultCountryOutlineColour()
392        {
393                return this.defaultCountryOutlineColour;
394        }
395
396        /**
397         * @param defaultCountryOutlineColour
398         *            the defaultCountryOutlineColour to set
399         */
400        public void setDefaultCountryOutlineColour(final Float[] defaultCountryOutlineColour)
401        {
402                this.defaultCountryOutlineColour = defaultCountryOutlineColour;
403        }
404
405        /**
406         * @return the defaultCountryLandColour
407         */
408        public Float[] getDefaultCountryLandColour()
409        {
410                return this.defaultCountryLandColour;
411        }
412
413        /**
414         * @param defaultCountryLandColour
415         *            the defaultCountryLandColour to set
416         */
417        public void setDefaultCountryLandColour(final Float[] defaultCountryLandColour)
418        {
419                this.defaultCountryLandColour = defaultCountryLandColour;
420        }
421
422        /**
423         * @return the highlightCountryLandColour
424         */
425        public Float[] getHighlightCountryLandColour()
426        {
427                return this.highlightCountryLandColour;
428        }
429
430        /**
431         * @param highlightCountryLandColour
432         *            the highlightCountryLandColour to set
433         */
434        public void setHighlightCountryLandColour(final Float[] highlightCountryLandColour)
435        {
436                this.highlightCountryLandColour = highlightCountryLandColour;
437        }
438
439        /**
440         * Returns a country code for a given country name.
441         * 
442         * @param countryName
443         *            The country name
444         * @return the country code
445         */
446        public String getCountryCodeByName(final String countryName)
447        {
448                final WorldPlace p = this.worldPolys.byCountry(countryName);
449                if (p == null)
450                        return null;
451                return p.getISOA2();
452        }
453
454        /**
455         * Returns the lat/long of a country given its country code
456         * 
457         * @param countryCode
458         *            The country code
459         * @return The lat long as a point2d
460         */
461        public Point2d getCountryLocation(final String countryCode)
462        {
463                final WorldPlace wp = this.worldPolys.byCountryCode(countryCode);
464                if (wp == null)
465                        return null;
466                return new Point2dImpl(wp.getLongitude(), wp.getLatitude());
467        }
468
469        @Override
470        public void clearData()
471        {
472                this.activeCountries.clear();
473                this.countryHighlightColours.clear();
474                super.clearData();
475        }
476
477        /**
478         * {@inheritDoc}
479         * 
480         * @see org.openimaj.vis.AnimatedVisualisationProvider#addAnimatedVisualisationListener(org.openimaj.vis.AnimatedVisualisationListener)
481         */
482        @Override
483        public void addAnimatedVisualisationListener(final AnimatedVisualisationListener avl)
484        {
485                this.listeners.add(avl);
486        }
487
488        /**
489         * {@inheritDoc}
490         * 
491         * @see org.openimaj.vis.AnimatedVisualisationProvider#removeAnimatedVisualisationListener(org.openimaj.vis.AnimatedVisualisationListener)
492         */
493        @Override
494        public void removeAnimatedVisualisationListener(final AnimatedVisualisationListener avl)
495        {
496                this.listeners.remove(avl);
497        }
498
499        /**
500         * Fire the animation event
501         */
502        protected void fireAnimationEvent()
503        {
504                for (final AnimatedVisualisationListener l : this.listeners)
505                        l.newVisualisationAvailable(this);
506        }
507
508        /**
509         * Animate the colour of a country
510         * 
511         * @param countryCode
512         *            The country to animate
513         * @param colourAnimator
514         *            The colour animator
515         */
516        public void animateCountryColour(final String countryCode, final ValueAnimator<Float[]> colourAnimator)
517        {
518                this.colourAnimators.put(countryCode, colourAnimator);
519                this.activeCountries.add(countryCode);
520                if (this.animationThread == null)
521                {
522                        this.animationThread = new Thread(new Runnable()
523                        {
524                                @Override
525                                public void run()
526                                {
527                                        while (!WorldMap.this.colourAnimators.isEmpty())
528                                        {
529                                                // Update the colour for each of the countries in our
530                                                // animators
531                                                final Iterator<String> caIt = WorldMap.this.colourAnimators.keySet().iterator();
532                                                while (caIt.hasNext())
533                                                {
534                                                        final String cc = caIt.next();
535
536                                                        // Get the next colour for the country and update
537                                                        // its highlight colour
538                                                        final Float[] colour = WorldMap.this.colourAnimators.get(cc).nextValue();
539                                                        WorldMap.this.countryHighlightColours.put(cc, colour);
540
541                                                        // If the animator's finished. we'll remove it
542                                                        if (WorldMap.this.colourAnimators.get(cc).hasFinished())
543                                                        {
544                                                                caIt.remove();
545
546                                                                if (Arrays.equals(colour, WorldMap.this.getDefaultCountryLandColour()))
547                                                                        WorldMap.this.activeCountries.remove(cc);
548                                                        }
549                                                }
550
551                                                // Fire the animation event
552                                                WorldMap.this.fireAnimationEvent();
553                                        }
554
555                                        WorldMap.this.animationThread = null;
556                                }
557                        });
558                        this.animationThread.start();
559                }
560        }
561
562        /**
563         * Demonstration method.
564         * 
565         * @param args
566         *            command-line args (unused)
567         */
568        public static void main(final String[] args)
569        {
570                final WorldMap<LabelledDot> wp = new WorldMap<LabelledDot>(
571                                1200, 800, new LabelledPointVisualisation());
572
573                // Show and highlight some stuff.
574                wp.addPoint(-67.271667, -55.979722, new LabelledDot("Cape Horn", 1d, RGBColour.WHITE));
575                wp.addPoint(-0.1275, 51.507222, new LabelledDot("London", 1d, RGBColour.WHITE));
576                wp.addPoint(139.6917, 35.689506, new LabelledDot("Tokyo", 1d, RGBColour.WHITE));
577                wp.addPoint(37.616667, 55.75, new LabelledDot("Moscow", 1d, RGBColour.WHITE));
578                wp.addHighlightCountry("cn");
579                wp.addHighlightCountry("us", new Float[] { 0f, 0.2f, 1f, 1f });
580                wp.getAxesRenderer().setDrawMajorTickGrid(true);
581                wp.showWindow("World");
582
583                /*
584                 * // Wait 3 seconds ... try { Thread.sleep( 3000 ); } catch( final
585                 * InterruptedException e ) {}
586                 * 
587                 * // ... and flash Russia wp.animateCountryColour( "ru", new
588                 * ColourSpaceAnimator( new Float[]{0.8f,1f,0.8f},
589                 * wp.getDefaultCountryLandColour(), 5000 ) );
590                 * 
591                 * // Wait another 2 seconds... try { Thread.sleep( 2000 ); } catch(
592                 * final InterruptedException e ) {}
593                 * 
594                 * // ... and flash Australia wp.animateCountryColour( "au", new
595                 * ColourSpaceAnimator( new Float[]{1f,0.8f,0.8f},
596                 * wp.getHighlightCountryLandColour(), 5000 ) );
597                 */}
598}