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}