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"]}
]
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.