Exemplo n.º 1
0
def test_tools_on_toggle_tool_action():
    action = tools.on_toggle_tool("time_series", False)
    assert action == {
        "kind": tools.ON_TOGGLE_TOOL,
        "tool_name": "time_series",
        "value": False
    }
Exemplo n.º 2
0
def main(argv=None):

    args = parse_args.parse_args(argv)
    data.AUTO_SHUTDOWN = args.auto_shutdown

    if len(args.files) > 0:
        if args.config_file is not None:
            raise Exception(
                '--config-file and [FILE [FILE ...]] not compatible')
        config = cfg.from_files(args.files, args.file_type)
    else:
        config = cfg.Config.load(args.config_file,
                                 variables=cfg.combine_variables(
                                     os.environ, args.variables))

    # Feature toggles
    if "feature" in config.plugins:
        features = plugin.call(config.plugins["feature"].entry_point)
    else:
        features = config.features
    data.FEATURE_FLAGS = features

    # Full screen map
    viewport = config.default_viewport
    x_range, y_range = geo.web_mercator(viewport.lon_range, viewport.lat_range)
    figure = map_figure(x_range, y_range)
    figures = [figure]
    for _ in range(2):
        f = map_figure(figure.x_range, figure.y_range)
        figures.append(f)

    figure_row = layers.FigureRow(figures)

    color_mapper = bokeh.models.LinearColorMapper(
        low=0, high=1, palette=bokeh.palettes.Plasma[256])

    # Convert config to datasets
    datasets = {}
    datasets_by_pattern = {}
    label_to_pattern = {}
    for group in config.file_groups:
        settings = {
            "label": group.label,
            "pattern": group.pattern,
            "locator": group.locator,
            "database_path": group.database_path,
            "directory": group.directory
        }
        dataset = drivers.get_dataset(group.file_type, settings)
        datasets[group.label] = dataset
        datasets_by_pattern[group.pattern] = dataset
        label_to_pattern[group.label] = group.pattern

    layers_ui = layers.LayersUI()

    # Add optional sub-navigators
    sub_navigators = {
        key: dataset.navigator()
        for key, dataset in datasets_by_pattern.items()
        if hasattr(dataset, "navigator")
    }
    navigator = navigate.Navigator(sub_navigators)

    middlewares = [
        keys.navigate,
        db.InverseCoordinate("pressure"),
        db.next_previous,
        db.Controls(navigator),  # TODO: Deprecate this middleware
        colors.palettes,
        colors.middleware(),
        presets.Middleware(presets.proxy_storage(config.presets_file)),
        presets.middleware,
        layers.middleware,
        navigator,
        mws.echo,
    ]
    store = redux.Store(forest.reducer, middlewares=middlewares)

    app = forest.app.Application()
    app.add_component(forest.components.title.Title())

    # Coastlines, borders, lakes and disputed borders
    view = forest.components.borders.View()
    for figure in figures:
        view.add_figure(figure)
    view.connect(store)
    border_ui = forest.components.borders.UI()
    border_ui.connect(store)

    # Colorbar user interface
    component = forest.components.ColorbarUI()
    app.add_component(component)

    # Add time user interface
    if config.defaults.timeui:
        component = forest.components.TimeUI()
        component.layout = bokeh.layouts.row(component.layout, name="time")
        app.add_component(component)

    # Connect MapView orchestration to store
    opacity_slider = forest.layers.OpacitySlider()
    source_limits = colors.SourceLimits().connect(store)
    factory_class = forest.layers.factory(color_mapper, figures, source_limits,
                                          opacity_slider)
    gallery = forest.layers.Gallery.from_datasets(datasets, factory_class)
    gallery.connect(store)

    # Connect layers controls
    layers_ui.add_subscriber(store.dispatch)
    layers_ui.connect(store)

    # Connect tools controls

    display_names = {
        "time_series": "Display Time Series",
        "profile": "Display Profile"
    }
    available_features = {
        k: display_names[k]
        for k in display_names.keys() if data.FEATURE_FLAGS[k]
    }

    tools_panel = tools.ToolsPanel(available_features)
    tools_panel.connect(store)

    # Navbar components
    navbar = Navbar(show_diagram_button=len(available_features) > 0)
    navbar.connect(store)

    # Connect tap listener
    tap_listener = screen.TapListener()
    tap_listener.connect(store)

    # Connect figure controls/views
    if config.defaults.figures.ui:
        figure_ui = layers.FigureUI(config.defaults.figures.maximum)
        figure_ui.connect(store)
    figure_row.connect(store)

    # Tiling picker
    if config.use_web_map_tiles:
        tile_picker = forest.components.TilePicker()
        for figure in figures:
            tile_picker.add_figure(figure)
        tile_picker.connect(store)

    # Connect color palette controls
    colors.ColorMapperView(color_mapper).connect(store)
    color_palette = colors.ColorPalette().connect(store)

    # Connect limit controllers to store
    user_limits = colors.UserLimits().connect(store)

    # Preset
    if config.defaults.presetui:
        preset_ui = presets.PresetUI().connect(store)

    # Connect navigation controls
    controls = db.ControlView()
    controls.connect(store)

    # Add support for a modal dialogue
    if data.FEATURE_FLAGS["multiple_colorbars"]:
        view = forest.components.modal.Tabbed()
    else:
        view = forest.components.modal.Default()
    modal = forest.components.Modal(view=view)
    modal.connect(store)

    # Connect components to Store
    app.connect(store)

    # Set initial state
    store.dispatch(forest.actions.set_state(config.state).to_dict())

    # Pre-select menu choices (if any)
    for pattern, _ in sub_navigators.items():
        state = db.initial_state(navigator, pattern=pattern)
        store.dispatch(forest.actions.update_state(state).to_dict())
        break

    # Set default time series visibility
    store.dispatch(tools.on_toggle_tool("time_series", False))

    # Set default profile visibility
    store.dispatch(tools.on_toggle_tool("profile", False))

    # Set top-level navigation
    store.dispatch(db.set_value("patterns", config.patterns))

    # Pre-select first map_view layer
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        for variable in navigator.variables(pattern):
            spec = {
                "label": label,
                "dataset": label,
                "variable": variable,
                "active": [0]
            }
            store.dispatch(forest.layers.save_layer(0, spec))
            break
        break

    # Set variable dimensions (needed by modal dialogue)
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        values = navigator.variables(pattern)
        store.dispatch(dimension.set_variables(label, values))

    # Organise controls/settings
    layouts = {}
    layouts["controls"] = []
    if config.defaults.figures.ui:
        layouts["controls"] += [
            bokeh.models.Div(text="Layout:"), figure_ui.layout
        ]
    layouts["controls"] += [
        bokeh.models.Div(text="Navigate:"), controls.layout,
        bokeh.models.Div(text="Compare:"), layers_ui.layout
    ]

    layouts["settings"] = [
        border_ui.layout,
        opacity_slider.layout,
        color_palette.layout,
        user_limits.layout,
        bokeh.models.Div(text="Tiles:"),
    ]
    if config.defaults.presetui:
        layouts["settings"].insert(2, preset_ui.layout)
    if config.use_web_map_tiles:
        layouts["settings"].append(tile_picker.layout)

    tabs = bokeh.models.Tabs(tabs=[
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["controls"]),
                           title="Control"),
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["settings"]),
                           title="Settings")
    ])

    tool_figures = {}
    if data.FEATURE_FLAGS["time_series"]:
        # Series sub-figure widget
        series_figure = bokeh.plotting.figure(plot_width=400,
                                              plot_height=200,
                                              x_axis_type="datetime",
                                              toolbar_location=None,
                                              border_fill_alpha=0)
        series_figure.toolbar.logo = None

        series_view = series.SeriesView.from_groups(series_figure,
                                                    config.file_groups)
        series_view.add_subscriber(store.dispatch)
        series_args = (rx.Stream().listen_to(store).map(
            series.select_args).filter(lambda x: x is not None).distinct())
        series_args.map(lambda a: series_view.render(*a))
        series_args.map(print)  # Note: map(print) creates None stream

        tool_figures["series_figure"] = series_figure

    if data.FEATURE_FLAGS["profile"]:
        # Profile sub-figure widget
        profile_figure = bokeh.plotting.figure(plot_width=300,
                                               plot_height=450,
                                               toolbar_location=None,
                                               border_fill_alpha=0)
        profile_figure.toolbar.logo = None
        profile_figure.y_range.flipped = True

        profile_view = profile.ProfileView.from_groups(profile_figure,
                                                       config.file_groups)
        profile_view.add_subscriber(store.dispatch)
        profile_args = (rx.Stream().listen_to(store).map(
            profile.select_args).filter(lambda x: x is not None).distinct())
        profile_args.map(lambda a: profile_view.render(*a))
        profile_args.map(print)  # Note: map(print) creates None stream

        tool_figures["profile_figure"] = profile_figure

    tool_layout = tools.ToolLayout(**tool_figures)
    tool_layout.connect(store)

    for f in figures:
        f.on_event(bokeh.events.Tap, tap_listener.update_xy)
        marker = screen.MarkDraw(f).connect(store)

    control_root = bokeh.layouts.column(tabs, name="controls")

    # Add key press support
    key_press = keys.KeyPress()
    key_press.add_subscriber(store.dispatch)

    # Add HTML ready support
    obj = html_ready.HTMLReady(key_press.hidden_button)
    obj.connect(store)

    document = bokeh.plotting.curdoc()
    document.title = "FOREST"
    document.add_root(control_root)
    document.add_root(
        bokeh.layouts.column(tools_panel.layout,
                             tool_layout.layout,
                             width=400,
                             name="series"))
    for root in navbar.roots:
        document.add_root(root)
    for root in app.roots:
        document.add_root(root)
    document.add_root(figure_row.layout)
    document.add_root(key_press.hidden_button)
    document.add_root(modal.layout)
Exemplo n.º 3
0
def main(argv=None):

    args = parse_args.parse_args(argv)
    data.AUTO_SHUTDOWN = args.auto_shutdown

    if len(args.files) > 0:
        if args.config_file is not None:
            raise Exception(
                '--config-file and [FILE [FILE ...]] not compatible')
        config = cfg.from_files(args.files, args.file_type)
    else:
        config = cfg.Config.load(args.config_file,
                                 variables=cfg.combine_variables(
                                     os.environ, args.variables))

    # Feature toggles
    if "feature" in config.plugins:
        features = plugin.call(config.plugins["feature"].entry_point)
    else:
        features = config.features
    data.FEATURE_FLAGS = features

    # Full screen map
    viewport = config.default_viewport
    x_range, y_range = geo.web_mercator(viewport.lon_range, viewport.lat_range)
    figure = bokeh.plotting.figure(
        x_range=x_range,
        y_range=y_range,
        #output_backend = "svg",
        x_axis_type="mercator",
        y_axis_type="mercator",
        active_scroll="wheel_zoom")

    figures = [figure]
    for _ in range(2):
        f = bokeh.plotting.figure(x_range=figure.x_range,
                                  y_range=figure.y_range,
                                  x_axis_type="mercator",
                                  y_axis_type="mercator",
                                  active_scroll="wheel_zoom")
        figures.append(f)

    for f in figures:
        f.axis.visible = False
        f.toolbar.logo = None
        f.toolbar_location = None
        f.min_border = 0

    figure_row = layers.FigureRow(figures)

    color_mapper = bokeh.models.LinearColorMapper(
        low=0, high=1, palette=bokeh.palettes.Plasma[256])

    # Convert config to datasets
    datasets = {}
    datasets_by_pattern = {}
    label_to_pattern = {}
    for group in config.file_groups:
        settings = {
            "label": group.label,
            "pattern": group.pattern,
            "locator": group.locator,
            "database_path": group.database_path,
            "directory": group.directory
        }
        dataset = drivers.get_dataset(group.file_type, settings)
        datasets[group.label] = dataset
        datasets_by_pattern[group.pattern] = dataset
        label_to_pattern[group.label] = group.pattern

    # Lakes
    for figure in figures:
        add_feature(figure, data.LAKES, color="lightblue")

    features = []
    for figure in figures:
        render2 = add_feature(figure, data.LAKES, color="lightblue")
        features += [
            add_feature(figure, data.COASTLINES),
            add_feature(figure, data.BORDERS)
        ]

    # Disputed borders
    for figure in figures:
        add_feature(figure, data.DISPUTED, color="red")

    toggle = bokeh.models.CheckboxGroup(labels=["Coastlines"],
                                        active=[0],
                                        width=135)

    def on_change(attr, old, new):
        if len(new) == 1:
            for feature in features:
                feature.visible = True
        else:
            for feature in features:
                feature.visible = False

    toggle.on_change("active", on_change)

    dropdown = bokeh.models.Dropdown(label="Color",
                                     menu=[("Black", "black"),
                                           ("White", "white")],
                                     width=50)
    autolabel(dropdown)

    def on_change(event):
        for feature in features:
            feature.glyph.line_color = new

    dropdown.on_click(on_change)

    layers_ui = layers.LayersUI()

    div = bokeh.models.Div(text="", width=10)
    border_row = bokeh.layouts.row(bokeh.layouts.column(toggle),
                                   bokeh.layouts.column(div),
                                   bokeh.layouts.column(dropdown))

    # Add optional sub-navigators
    sub_navigators = {
        key: dataset.navigator()
        for key, dataset in datasets_by_pattern.items()
        if hasattr(dataset, "navigator")
    }
    navigator = navigate.Navigator(sub_navigators)

    # Pre-select menu choices (if any)
    initial_state = {}
    for pattern, _ in sub_navigators.items():
        initial_state = db.initial_state(navigator, pattern=pattern)
        break

    middlewares = [
        keys.navigate,
        db.InverseCoordinate("pressure"),
        db.next_previous,
        db.Controls(navigator),  # TODO: Deprecate this middleware
        colors.palettes,
        colors.middleware(),
        presets.Middleware(presets.proxy_storage(config.presets_file)),
        presets.middleware,
        layers.middleware,
        navigator,
        mws.echo,
    ]
    store = redux.Store(forest.reducer,
                        initial_state=initial_state,
                        middlewares=middlewares)

    # Colorbar user interface
    colorbar_ui = forest.components.ColorbarUI()
    colorbar_ui.connect(store)

    # Add time user interface
    time_ui = forest.components.TimeUI()
    time_ui.connect(store)

    # Connect MapView orchestration to store
    opacity_slider = forest.layers.OpacitySlider()
    source_limits = colors.SourceLimits().connect(store)
    factory_class = forest.layers.factory(color_mapper, figures, source_limits,
                                          opacity_slider)
    gallery = forest.layers.Gallery.from_datasets(datasets, factory_class)
    gallery.connect(store)

    # Connect layers controls
    layers_ui.add_subscriber(store.dispatch)
    layers_ui.connect(store)

    # Connect tools controls

    display_names = {
        "time_series": "Display Time Series",
        "profile": "Display Profile",
        "barc": "BARC Toolkit"
    }
    available_features = {
        k: display_names[k]
        for k in display_names.keys() if data.FEATURE_FLAGS[k]
    }

    tools_panel = tools.ToolsPanel(available_features)
    tools_panel.connect(store)

    #barc_toolbar=bokeh.models.tools.Toolbar(tools=barc_tools,logo=None)
    if data.FEATURE_FLAGS["BARC"]:
        barc = BARC(figures)
        tools_panel.layout.children.extend(barc.ToolBar())

    # Navbar components
    navbar = Navbar(show_diagram_button=len(available_features) > 0)
    navbar.connect(store)

    # Connect tap listener
    tap_listener = screen.TapListener()
    tap_listener.connect(store)

    # Connect figure controls/views
    figure_ui = layers.FigureUI()
    figure_ui.add_subscriber(store.dispatch)
    figure_row.connect(store)

    # Tiling picker
    if config.use_web_map_tiles:
        tile_picker = forest.components.TilePicker()
        for figure in figures:
            tile_picker.add_figure(figure)
        tile_picker.connect(store)

    # Connect color palette controls
    colors.ColorMapperView(color_mapper).connect(store)
    color_palette = colors.ColorPalette().connect(store)

    # Connect limit controllers to store
    user_limits = colors.UserLimits().connect(store)

    # Preset
    preset_ui = presets.PresetUI().connect(store)

    # Connect navigation controls
    controls = db.ControlView()
    controls.connect(store)

    # Add support for a modal dialogue
    if data.FEATURE_FLAGS["multiple_colorbars"]:
        view = forest.components.modal.Tabbed()
    else:
        view = forest.components.modal.Default()
    modal = forest.components.Modal(view=view)
    modal.connect(store)

    # Set default time series visibility
    store.dispatch(tools.on_toggle_tool("time_series", False))

    # Set default profile visibility
    store.dispatch(tools.on_toggle_tool("profile", False))

    # Set top-level navigation
    store.dispatch(db.set_value("patterns", config.patterns))

    # Pre-select first map_view layer
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        for variable in navigator.variables(pattern):
            spec = {
                "label": label,
                "dataset": label,
                "variable": variable,
                "active": [0]
            }
            store.dispatch(forest.layers.save_layer(0, spec))
            break
        break

    # Set variable dimensions (needed by modal dialogue)
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        values = navigator.variables(pattern)
        store.dispatch(dimension.set_variables(label, values))

    # Select web map tiling
    if config.use_web_map_tiles:
        store.dispatch(tiles.set_tile(tiles.STAMEN_TERRAIN))
        store.dispatch(tiles.set_label_visible(True))

    # Organise controls/settings
    layouts = {}
    layouts["controls"] = [
        bokeh.models.Div(text="Layout:"), figure_ui.layout,
        bokeh.models.Div(text="Navigate:"), controls.layout,
        bokeh.models.Div(text="Compare:"), layers_ui.layout
    ]
    layouts["settings"] = [
        border_row,
        opacity_slider.layout,
        preset_ui.layout,
        color_palette.layout,
        user_limits.layout,
        bokeh.models.Div(text="Tiles:"),
    ]
    if config.use_web_map_tiles:
        layouts["settings"].append(tile_picker.layout)

    tabs = bokeh.models.Tabs(tabs=[
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["controls"]),
                           title="Control"),
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["settings"]),
                           title="Settings")
    ])

    tool_figures = {}
    if data.FEATURE_FLAGS["time_series"]:
        # Series sub-figure widget
        series_figure = bokeh.plotting.figure(plot_width=400,
                                              plot_height=200,
                                              x_axis_type="datetime",
                                              toolbar_location=None,
                                              border_fill_alpha=0)
        series_figure.toolbar.logo = None

        series_view = series.SeriesView.from_groups(series_figure,
                                                    config.file_groups)
        series_view.add_subscriber(store.dispatch)
        series_args = (rx.Stream().listen_to(store).map(
            series.select_args).filter(lambda x: x is not None).distinct())
        series_args.map(lambda a: series_view.render(*a))
        series_args.map(print)  # Note: map(print) creates None stream

        tool_figures["series_figure"] = series_figure

    if data.FEATURE_FLAGS["profile"]:
        # Profile sub-figure widget
        profile_figure = bokeh.plotting.figure(plot_width=300,
                                               plot_height=450,
                                               toolbar_location=None,
                                               border_fill_alpha=0)
        profile_figure.toolbar.logo = None
        profile_figure.y_range.flipped = True

        profile_view = profile.ProfileView.from_groups(profile_figure,
                                                       config.file_groups)
        profile_view.add_subscriber(store.dispatch)
        profile_args = (rx.Stream().listen_to(store).map(
            profile.select_args).filter(lambda x: x is not None).distinct())
        profile_args.map(lambda a: profile_view.render(*a))
        profile_args.map(print)  # Note: map(print) creates None stream

        tool_figures["profile_figure"] = profile_figure

    tool_layout = tools.ToolLayout(**tool_figures)
    tool_layout.connect(store)

    for f in figures:
        f.on_event(bokeh.events.Tap, tap_listener.update_xy)
        marker = screen.MarkDraw(f).connect(store)

    control_root = bokeh.layouts.column(tabs, name="controls")

    # Add key press support
    key_press = keys.KeyPress()
    key_press.add_subscriber(store.dispatch)

    document = bokeh.plotting.curdoc()
    document.title = "FOREST"
    document.add_root(control_root)
    document.add_root(
        bokeh.layouts.column(tools_panel.layout,
                             tool_layout.layout,
                             width=400,
                             name="series"))
    document.add_root(bokeh.layouts.row(time_ui.layout, name="time"))
    for root in navbar.roots:
        document.add_root(root)
    document.add_root(colorbar_ui.layout)
    document.add_root(figure_row.layout)
    document.add_root(key_press.hidden_button)
    document.add_root(modal.layout)
Exemplo n.º 4
0
def test_tool_toggle_reducer():
    state = tools.reducer({}, tools.on_toggle_tool("time_series", True))
    assert state == {"tools": {"time_series": True}}
Exemplo n.º 5
0
def test_tools_panel_on_toggle_emits_action():
    listener = unittest.mock.Mock()
    tools_panel = tools.ToolsPanel({"time_series": "wheeeyyyy"})
    tools_panel.add_subscriber(listener)
    tools_panel.on_click("time_series")(True)
    listener.assert_called_once_with(tools.on_toggle_tool("time_series", True))
Exemplo n.º 6
0
def test_tool_toggle_reducer_immutable_state():
    state = {"tools": {"time_series": True}}
    next_state = tools.reducer(state,
                               tools.on_toggle_tool("time_series", False))
    assert state == {"tools": {"time_series": True}}
    assert next_state == {"tools": {"time_series": False}}
Exemplo n.º 7
0
def test_tool_toggle_reducer():
    state = tools.reducer({}, tools.on_toggle_tool("time_series", True))
    assert state == {"tools": {"time_series": True}}


def test_tool_toggle_reducer_immutable_state():
    state = {"tools": {"time_series": True}}
    next_state = tools.reducer(state,
                               tools.on_toggle_tool("time_series", False))
    assert state == {"tools": {"time_series": True}}
    assert next_state == {"tools": {"time_series": False}}


@pytest.mark.parametrize("actions,expect", [
    ([], {}),
    ([tools.on_toggle_tool("profile", False)], {
        "tools": {
            "profile": False
        }
    }),
    ([db.set_value("key", "value")], {
        "key": "value"
    }),
    ([
        tools.on_toggle_tool("time_series", False),
        db.set_value("key", "value")
    ], {
        "key": "value",
        "tools": {
            "time_series": False
        }
Exemplo n.º 8
0
def main(argv=None):
    config = configure(argv=argv)

    # Feature toggles
    if "feature" in config.plugins:
        features = plugin.call(config.plugins["feature"].entry_point)
    else:
        features = config.features
    data.FEATURE_FLAGS = features

    # Full screen map
    viewport = config.default_viewport
    x_range, y_range = geo.web_mercator(viewport.lon_range, viewport.lat_range)

    figure = map_figure(x_range, y_range)
    figures = [figure]
    for _ in range(2):
        f = map_figure(figure.x_range, figure.y_range)
        figures.append(f)

    figure_row = layers.FigureRow(figures)

    color_mapper = bokeh.models.LinearColorMapper(
        low=0, high=1, palette=bokeh.palettes.Plasma[256])

    # Convert config to datasets
    datasets = {}
    datasets_by_pattern = {}
    label_to_pattern = {}
    for group, dataset in zip(config.file_groups, config.datasets):
        datasets[group.label] = dataset
        datasets_by_pattern[group.pattern] = dataset
        label_to_pattern[group.label] = group.pattern

    # print('\n\n\n\n', datasets, '\n\n\n\n')
    '''# Lakes
    for figure in figures:
        add_feature(figure, data.LAKES, color="lightblue")

    features = []
    for figure in figures:
        render2 = add_feature(figure, data.LAKES, color="lightblue")
        features += [
            add_feature(figure, data.COASTLINES),
            add_feature(figure, data.BORDERS)]

    # Disputed borders
    for figure in figures:
        add_feature(figure, data.DISPUTED, color="red")
    '''

    toggle = bokeh.models.CheckboxGroup(labels=["Coastlines"],
                                        active=[0],
                                        width=135)

    def on_change(attr, old, new):
        if len(new) == 1:
            for feature in features:
                feature.visible = True
        else:
            for feature in features:
                feature.visible = False

    toggle.on_change("active", on_change)

    dropdown = bokeh.models.Dropdown(label="Color",
                                     menu=[("Black", "black"),
                                           ("White", "white")],
                                     width=50)
    autolabel(dropdown)

    def on_change(event):
        for feature in features:
            feature.glyph.line_color = new

    dropdown.on_click(on_change)

    layers_ui = layers.LayersUI()

    # Add optional sub-navigators
    sub_navigators = {
        key: dataset.navigator()
        for key, dataset in datasets_by_pattern.items()
        if hasattr(dataset, "navigator")
    }
    navigator = navigate.Navigator(sub_navigators)

    middlewares = [
        keys.navigate,
        db.InverseCoordinate("pressure"),
        db.next_previous,
        db.Controls(navigator),  # TODO: Deprecate this middleware
        colors.palettes,
        colors.middleware(),
        presets.Middleware(presets.proxy_storage(config.presets_file)),
        presets.middleware,
        layers.middleware,
        navigator,
        mws.echo,
    ]
    store = redux.Store(forest.reducer, middlewares=middlewares)

    app = forest.app.Application()
    app.add_component(forest.components.title.Title())

    # Coastlines, borders, lakes and disputed borders
    view = forest.components.borders.View()
    for figure in figures:
        view.add_figure(figure)
    view.connect(store)
    border_ui = forest.components.borders.UI()
    border_ui.connect(store)

    # Colorbar user interface
    component = forest.components.ColorbarUI()
    app.add_component(component)

    # Add time user interface
    if config.defaults.timeui:
        component = forest.components.TimeUI()
        component.layout = bokeh.layouts.row(component.layout, name="time")
        app.add_component(component)

    # Connect MapView orchestration to store
    opacity_slider = forest.layers.OpacitySlider()
    source_limits = colors.SourceLimits().connect(store)
    factory_class = forest.layers.factory(color_mapper, figures, source_limits,
                                          opacity_slider)
    gallery = forest.gallery.Gallery.map_view(datasets, factory_class)
    gallery.connect(store)

    # Connect layers controls
    layers_ui.add_subscriber(store.dispatch)
    layers_ui.connect(store)

    # Connect tools controls

    display_names = {
        "time_series": "Display Time Series",
        "profile": "Display Profile",
        "barc": "BARC Toolkit"
    }
    available_features = {
        k: display_names[k]
        for k in display_names.keys() if data.FEATURE_FLAGS[k]
    }

    tools_panel = tools.ToolsPanel(available_features)
    tools_panel.connect(store)

    #barc_toolbar=bokeh.models.tools.Toolbar(tools=barc_tools,logo=None)
    if data.FEATURE_FLAGS["BARC"]:
        barc = BARC(figures)
        tools_panel.layout.children.append(barc.ToolBar())

    # Navbar components
    navbar = Navbar(show_diagram_button=len(available_features) > 0)
    navbar.connect(store)

    # Connect tap listener
    tap_listener = screen.TapListener()
    tap_listener.connect(store)

    # Connect figure controls/views
    if config.defaults.figures.ui:
        figure_ui = layers.FigureUI(config.defaults.figures.maximum)
        figure_ui.connect(store)
    figure_row.connect(store)

    # Tiling picker
    if config.use_web_map_tiles:
        tile_picker = forest.components.TilePicker()
        for figure in figures:
            tile_picker.add_figure(figure)

        tile_picker.connect(store)

    if not data.FEATURE_FLAGS["multiple_colorbars"]:
        # Connect color palette controls
        colors.ColorMapperView(color_mapper).connect(store)
        color_palette = colors.ColorPalette().connect(store)

        # Connect limit controllers to store
        user_limits = colors.UserLimits().connect(store)

    # Preset
    if config.defaults.presetui:
        preset_ui = presets.PresetUI().connect(store)

    # Connect navigation controls
    controls = db.ControlView()
    controls.connect(store)

    # Add support for a modal dialogue
    if data.FEATURE_FLAGS["multiple_colorbars"]:
        view = forest.components.modal.Tabbed()
    else:
        view = forest.components.modal.Default()
    modal = forest.components.Modal(view=view)
    modal.connect(store)

    # Connect components to Store
    app.connect(store)

    # Set initial state
    store.dispatch(forest.actions.set_state(config.state).to_dict())

    # Pre-select menu choices (if any)
    for pattern, _ in sub_navigators.items():
        state = db.initial_state(navigator, pattern=pattern)
        store.dispatch(forest.actions.update_state(state).to_dict())
        break

    # Set default time series visibility
    store.dispatch(tools.on_toggle_tool("time_series", False))

    # Set default profile visibility
    store.dispatch(tools.on_toggle_tool("profile", False))

    # Set top-level navigation
    store.dispatch(db.set_value("patterns", config.patterns))

    # Pre-select first map_view layer
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        for variable in navigator.variables(pattern):
            spec = {
                "label": label,
                "dataset": label,
                "variable": variable,
                "active": [0]
            }
            store.dispatch(forest.layers.save_layer(0, spec))
            break
        break

    # Set variable dimensions (needed by modal dialogue)
    for label, dataset in datasets.items():
        pattern = label_to_pattern[label]
        values = navigator.variables(pattern)
        store.dispatch(dimension.set_variables(label, values))

    # Organise controls/settings
    layouts = {}
    layouts["controls"] = []
    if config.defaults.figures.ui:
        layouts["controls"] += [
            bokeh.models.Div(text="Layout:"), figure_ui.layout
        ]
    layouts["controls"] += [
        bokeh.models.Div(text="Navigate:"), controls.layout,
        bokeh.models.Div(text="Compare:"), layers_ui.layout
    ]

    layouts["settings"] = [
        bokeh.models.Div(text="Borders, coastlines and lakes:"),
        border_ui.layout,
        opacity_slider.layout,
    ]
    if not data.FEATURE_FLAGS["multiple_colorbars"]:
        layouts["settings"].append(color_palette.layout)
        layouts["settings"].append(user_limits.layout)
    if config.defaults.presetui:
        layouts["settings"].append(preset_ui.layout)
    if config.use_web_map_tiles:
        layouts["settings"].append(bokeh.models.Div(text="Tiles:"))
        layouts["settings"].append(tile_picker.layout)

    tabs = bokeh.models.Tabs(tabs=[
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["controls"]),
                           title="Control"),
        bokeh.models.Panel(child=bokeh.layouts.column(*layouts["settings"]),
                           title="Settings")
    ])

    tool_figures = {}
    if data.FEATURE_FLAGS["time_series"]:
        # Series sub-figure widget
        series_figure = bokeh.plotting.figure(plot_width=400,
                                              plot_height=200,
                                              x_axis_type="datetime",
                                              toolbar_location=None,
                                              border_fill_alpha=0)
        series_figure.toolbar.logo = None

        gallery = forest.gallery.Gallery.series_view(datasets, series_figure)
        gallery.connect(store)

        tool_figures["series_figure"] = series_figure

    if data.FEATURE_FLAGS["profile"]:
        # Profile sub-figure widget
        profile_figure = bokeh.plotting.figure(plot_width=300,
                                               plot_height=450,
                                               toolbar_location=None,
                                               border_fill_alpha=0)
        profile_figure.toolbar.logo = None
        profile_figure.y_range.flipped = True

        gallery = forest.gallery.Gallery.profile_view(datasets, profile_figure)
        gallery.connect(store)

        tool_figures["profile_figure"] = profile_figure

    tool_layout = tools.ToolLayout(**tool_figures)
    tool_layout.connect(store)

    for f in figures:
        f.on_event(bokeh.events.Tap, tap_listener.update_xy)
        marker = screen.MarkDraw(f).connect(store)

    control_root = bokeh.layouts.column(tabs, name="controls")

    # Add key press support
    key_press = keys.KeyPress()
    key_press.add_subscriber(store.dispatch)

    # Add HTML ready support
    obj = html_ready.HTMLReady(key_press.hidden_button)
    obj.connect(store)

    document = bokeh.plotting.curdoc()
    document.title = "FOREST"
    document.add_root(control_root)
    document.add_root(
        bokeh.layouts.column(tools_panel.layout,
                             tool_layout.layout,
                             width=400,
                             name="series"))
    for root in navbar.roots:
        document.add_root(root)
    for root in app.roots:
        document.add_root(root)
    document.add_root(figure_row.layout)
    document.add_root(key_press.hidden_button)
    document.add_root(modal.layout)