10
$\begingroup$

Let's say I have a map of Europe with a locator in manipulate

Manipulate[ GeoGraphics["Europe", GeoBackground -> "CountryBorders"], {{p, {150, 150}}, Locator}]

enter image description here

Currently I can use the locator to navigate aimlessly around the map. My goal would be to select the country the mouse clicks on, where I am able to process which country has be selected and it is highlighted. It is unnecessary to have a locator, it was the only method I know.

I am aware this is an extensive question and am happy for any advice.

$\endgroup$

1 Answer 1

12
$\begingroup$

Here is an approach using DynamicModule, together with EventHandler wrappers around the individual Polygon directives. It also incorporates a lot of front-end trickery to get a user control that runs entirely in the front-end (avoiding front-end ↔ kernel communication improves the performance significantly, especially for cases like this where a many complex graphics primitives are used)

CountrySelector[c_, opts : OptionsPattern[]] := 
 iCountrySelector[#, c, opts] &
CountrySelector[Dynamic@var_, c_, opts : OptionsPattern[]] := 
 iCountrySelector[Dynamic@var, c, opts]
iCountrySelector[Dynamic@var_, c_, opts : OptionsPattern[]] :=
 DynamicModule[
  {
    control = Panel@Row@{
      "Loading country selector...",
      ProgressIndicator[Appearance -> "Percolate"]
     }
   },
  Dynamic@control,
  Initialization :> SessionSubmit[
    control = DynamicModule[
      {active, countries = c},
      GeoGraphics[
       {
        MapIndexed[
         EventHandler[
           Style[
            Annotation[Polygon@#, ""],
            TagBoxOptions -> {
              BaseStyle -> FEPrivate`Which[
                FEPrivate`SameQ[active, #2[[1]]],
                {Lighter@Red, EdgeForm@{Thick, Red}},
                FrontEnd`CurrentValue@"MouseOver",
                EdgeForm@Lighter@Red,
                True,
                {}
                ]
              }
            ],
           {
            "MouseClicked" :> (FEPrivate`Set[active, #2[[1]]]; var = #)
            }
           ] &,
         countries
         ]
        },
       opts,
       GeoBackground -> "CountryBorders",
       ImageSize -> Medium
       ]
      ]
    ]
  ]

The control is written to support use as a custom control type for Manipulate:

Manipulate[
 Dynamic@country,
 {{country, None}, CountrySelector@EntityList@EntityClass["Country", "Europe"]}
 ]

Image

Standalone use would look like this:

CountrySelector[Dynamic@country, EntityList@EntityClass["Country", "Europe"]}]

You can also pass in options to be added to the GeoGraphics command, e.g. if you want to change the size or the GeoBackground.

Some notes on the implementation:

  • The helper function iCountrySelector is needed to ensure that Manipulate sees a Function expression, otherwise it will not use the custom control type.
  • Since we need GeoGraphics to "see" the Polygon[...] directives, we cannot wrap them in Dynamic (also, this wouldn't be good for performance). Instead, we have to find a way to apply the styling as an option. The best I could come up with is TagBoxOptions->{BaseStyle->…} together with an Annotation wrapper (which produces the TagBox). One could also use PolygonBoxOptions to style the Polygon directives directly, but apparently, FilledCurve is used for some countries (which would require us to set FilledCurveBoxOptions as well).
  • The state of the control (i.e. which country is selected) is stored in a DynamicModule variable (called active in the code above)
  • We use a separate active variable for the control itself. This ensures that the front-end does everything on its own without calling the kernel. (When using a single variable, I think the problem is that the front-end sees the variable used in a complicated Dynamic expression, which causes it to do everything over the kernel)
  • FEPrivate`Set is used to set the variable via the front-end, eliminating the need for kernel-communication.
  • FEPrivate`Which and FEPrivate`SameQ are needed in order to ensure that the style computation can be done by the front-end. (the functions are "documented" here)
  • We use the country index instead of the entity itself for active, since the front-end needs to call the kernel otherwise as far as I can tell.
  • Since the setup time can be quite long (especially if the country data needs to be downloaded first, the Manipulate is prone to timing out, returning $Aborted. To fix this, we display a placeholder control, and use SessionSubmit to complete the control construction asynchronously. We use Initialization to ensure that the control is actually updated once it is finished, as explained here.
$\endgroup$
4
  • $\begingroup$ This looks good, unfortunately when I run it in version 12.0 the output is Aborted. Any thoughts why? $\endgroup$
    – Tim B
    Commented Dec 1, 2019 at 15:47
  • $\begingroup$ Hm... It looks like the initial setup time is too long for Manipulate, causing it to time out and return $Aborted - I have updated the answer with a workaround (note that the first time it might take quite a while, until everything has been downloaded) $\endgroup$
    – Lukas Lang
    Commented Dec 1, 2019 at 16:28
  • $\begingroup$ Wonderful, it works now, thank you. One final question, I am aware this wasn't in the question, but what is the simplest way to CloudDeploy this? As it has been crashing in the ways I tried. $\endgroup$
    – Tim B
    Commented Dec 1, 2019 at 18:28
  • 1
    $\begingroup$ @TimB unfortunately, I don't see an easy way to adapt the code as it is written now, and I'm not accustomed enough with the cloud stuff to be able to come up with a performant solution. I would therefore suggest you to create a separate question for that $\endgroup$
    – Lukas Lang
    Commented Dec 1, 2019 at 19:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.