Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: enhance Map control #3994

Merged
merged 15 commits into from
Oct 19, 2024
Merged

feat!: enhance Map control #3994

merged 15 commits into from
Oct 19, 2024

Conversation

ndonkoHenri
Copy link
Contributor

@ndonkoHenri ndonkoHenri commented Sep 15, 2024

Description

Closes #3688

Code

Test Code

import flet as ft
import flet.map as map
import random
import re

INITIAL_CENTER = map.MapLatitudeLongitude(latitude=17.701, longitude=11.44)
INITIAL_ZOOM = 4
TEXT_FIELD_ERROR_MSG = "Invalid input!"
TEXT_FIELD_INPUT_FILTER_REGEX = r"^\s*[+-]?\d*\.?\d*\s*$"


class MyTooltip(ft.Tooltip):
    """Custom tooltip with no wait duration and no preference to show above the control."""

    def __init__(self, text: str):
        super().__init__(text, wait_duration=0, prefer_below=False)


def main(page: ft.Page):
    page.padding = ft.Padding(2, 0, 2, 0)
    # on web, disable the browser context menu
    if page.web:
        page.browser_context_menu.disable()

    # refs
    marker_layer_ref = ft.Ref[map.MarkerLayer]()
    map_ref = ft.Ref[map.Map]()
    user_latitude_ref = ft.Ref[ft.TextField]()
    user_longitude_ref = ft.Ref[ft.TextField]()

    last_moved_to_marker_index = 0

    def handle_go_to_dlg_coordinates(e: ft.ControlEvent):
        lat = user_latitude_ref.current.value.rstrip(".")
        long = user_longitude_ref.current.value.rstrip(".")

        has_error = False

        # Validate latitude
        if not bool(re.match(r"[-+]?[0-9]*\.?[0-9]+", lat)):
            user_latitude_ref.current.error_text = TEXT_FIELD_ERROR_MSG
            has_error = True

        # Validate longitude
        if not bool(re.match(r"[-+]?[0-9]*\.?[0-9]+", long)):
            user_longitude_ref.current.error_text = TEXT_FIELD_ERROR_MSG
            has_error = True

        if not has_error:
            page.close(coordinates_dlg)
            map_ref.current.move_to(
                destination=map.MapLatitudeLongitude(
                    latitude=float(lat), longitude=float(long)
                ),
                zoom=INITIAL_ZOOM,
                animation_duration=ft.Duration(milliseconds=2000),
            )
            user_latitude_ref.current.error_text = None
            user_longitude_ref.current.error_text = None
        page.update()

    def handle_tap(e: map.MapTapEvent):
        """Handle tap event on the map. Adds a marker at the tapped location."""
        marker_layer_ref.current.markers.append(
            map.Marker(
                coordinates=e.coordinates,
                content=ft.GestureDetector(
                    mouse_cursor=ft.MouseCursor.CLICK,
                    content=ft.Icon(
                        name=ft.icons.LOCATION_ON,
                        size=68,
                        color=ft.cupertino_colors.BLACK,
                        tooltip=MyTooltip(
                            text=f"{round(e.coordinates.latitude, 3)}, {round(e.coordinates.longitude, 3)}"
                        ),
                    ),
                    on_tap=lambda _: map_ref.current.move_to(
                        destination=e.coordinates,
                        animation_duration=ft.Duration(milliseconds=2000),
                    ),
                ),
            )
        )
        page.update()

    def handle_clear_markers(e: ft.ControlEvent):
        """Clears all markers on the map."""
        nonlocal last_moved_to_marker_index
        marker_layer_ref.current.markers = []
        last_moved_to_marker_index = 0
        page.update()

    def handle_move_to_next_marker(e: ft.ControlEvent):
        """Moves the map to the next marker in the marker layer."""
        nonlocal last_moved_to_marker_index
        markers = marker_layer_ref.current.markers
        if len(markers) > 0:
            last_moved_to_marker_index = (last_moved_to_marker_index + 1) % len(markers)
            map_ref.current.move_to(
                destination=markers[last_moved_to_marker_index].coordinates,
                animation_duration=ft.Duration(milliseconds=2000),
            )
            page.update()

    coordinates_dlg = ft.AlertDialog(
        adaptive=True,
        title=ft.Text("Enter the coordinates"),
        content=ft.Column(
            tight=True,
            controls=[
                ft.TextField(
                    ref=user_latitude_ref,
                    label="Latitude",
                    input_filter=ft.InputFilter(
                        regex_string=TEXT_FIELD_INPUT_FILTER_REGEX
                    ),
                ),
                ft.TextField(
                    ref=user_longitude_ref,
                    label="Longitude",
                    input_filter=ft.InputFilter(
                        regex_string=TEXT_FIELD_INPUT_FILTER_REGEX
                    ),
                ),
            ],
        ),
        actions=[
            ft.TextButton(
                text="Cancel", on_click=lambda e: page.close(e.control.parent)
            ),
            ft.TextButton(text="GO", on_click=handle_go_to_dlg_coordinates),
        ],
    )
    page.add(
        ft.Stack(
            expand=True,
            controls=[
                map.Map(
                    ref=map_ref,
                    on_tap=handle_tap,
                    configuration=map.MapConfiguration(
                        initial_center=INITIAL_CENTER,
                        initial_zoom=INITIAL_ZOOM,
                        min_zoom=1.3,
                        animation_duration=ft.Duration(milliseconds=1500),
                        interaction_configuration=map.MapInteractionConfiguration(
                            flags=map.MapInteractiveFlag.ALL
                            & ~map.MapInteractiveFlag.DOUBLE_TAP_ZOOM  # prevent double tap zoom for quicker on-tap calls
                        ),
                    ),
                    layers=[
                        map.TileLayer(
                            url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
                            on_image_error=lambda e: print("TileLayer Error"),
                        ),
                        map.MarkerLayer(
                            ref=marker_layer_ref,
                            markers=[],
                        ),
                    ],
                ),
                ft.Row(
                    expand=True,
                    bottom=16,
                    left=40,
                    spacing=16,
                    alignment=ft.MainAxisAlignment.CENTER,
                    controls=[
                        ft.FloatingActionButton(
                            icon=ft.icons.ZOOM_IN,
                            tooltip=MyTooltip("Zoom in"),
                            on_click=lambda e: map_ref.current.zoom_in(
                                animation_duration=ft.Duration(milliseconds=2500)
                            ),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.ZOOM_OUT,
                            tooltip=MyTooltip("Zoom out"),
                            on_click=lambda e: map_ref.current.zoom_out(
                                animation_duration=ft.Duration(milliseconds=2500)
                            ),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.ROTATE_RIGHT,
                            tooltip=MyTooltip("Rotate 90°"),
                            on_click=lambda e: map_ref.current.rotate_from(degree=90),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.ROTATE_LEFT,
                            tooltip=MyTooltip("Rotate -90°"),
                            on_click=lambda e: map_ref.current.rotate_from(degree=-90),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.CROP_ROTATE,
                            tooltip=MyTooltip("Reset Rotation"),
                            on_click=lambda e: map_ref.current.reset_rotation(
                                animation_duration=ft.Duration(milliseconds=2000)
                            ),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.CENTER_FOCUS_STRONG,
                            tooltip=MyTooltip("Center"),
                            on_click=lambda e: map_ref.current.move_to(
                                destination=INITIAL_CENTER,
                                zoom=INITIAL_ZOOM,
                            ),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.EXPLORE,
                            tooltip=MyTooltip("Random Location"),
                            on_click=lambda e: map_ref.current.move_to(
                                destination=map.MapLatitudeLongitude(
                                    latitude=random.uniform(-90, 90),
                                    longitude=random.uniform(-180, 180),
                                ),
                                zoom=random.uniform(3, 6),
                                animation_duration=ft.Duration(milliseconds=4000),
                            ),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.POLYLINE_ROUNDED,
                            tooltip=MyTooltip("Move to next marker"),
                            on_click=handle_move_to_next_marker,
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.SKIP_NEXT,
                            tooltip=MyTooltip("Move To"),
                            on_click=lambda e: page.open(coordinates_dlg),
                        ),
                        ft.FloatingActionButton(
                            icon=ft.icons.CLEAR,
                            tooltip=MyTooltip("Clear Markers"),
                            on_click=handle_clear_markers,
                        ),
                    ],
                ),
            ],
        )
    )


ft.app(main)

Summary by Sourcery

Enhance the Map control by adding new event handlers and methods for map interactions and animations. Refactor the MapConfiguration class and improve JSON parsing functions. Introduce animated map control and marker layers using new dependencies.

New Features:

  • Introduce new event handlers for map interactions, including tap, hover, secondary tap, long press, and pointer events.
  • Add new methods for map control such as rotate_from, reset_rotation, zoom_in, zoom_out, zoom_to, move_to, and center_on.
  • Implement animated map control using the AnimatedMapController for smooth transitions and animations.
  • Add support for animated markers and layers using the flutter_map_animations package.

Enhancements:

  • Refactor the MapConfiguration class to use a dataclass for cleaner and more efficient attribute management.
  • Improve the handling of optional values in JSON parsing functions by providing default values.
  • Enhance the map's tile layer with a cancellable network tile provider and fade-in tile display.

Chores:

  • Remove unused children parameters from various control classes to simplify the codebase.
  • Update the pubspec.yaml to include new dependencies for map animations and tile provider.

Copy link
Contributor

sourcery-ai bot commented Sep 15, 2024

Reviewer's Guide by Sourcery

This pull request significantly enhances the Map control in the Flet framework. It introduces new features, improves existing functionality, and refactors the codebase for better performance and maintainability. The changes primarily focus on adding new map interaction capabilities, improving event handling, and updating the underlying map library dependencies.

File-Level Changes

Change Details Files
Enhanced Map control with new interaction methods and events
  • Added new methods like rotate_from, reset_rotation, zoom_in, zoom_out, zoom_to, move_to, and center_on
  • Implemented new event handlers for various map interactions
  • Refactored the Map class to include these new methods and event handlers
sdk/python/packages/flet-core/src/flet_core/map/map.py
Refactored MapConfiguration to improve code organization
  • Moved MapConfiguration from a separate control to a dataclass
  • Removed redundant event handlers from MapConfiguration
  • Simplified the configuration parsing process
sdk/python/packages/flet-core/src/flet_core/map/map_configuration.py
packages/flet_map/lib/src/map.dart
Updated map-related dependencies and implementations
  • Replaced flutter_map with flutter_map_animations for smoother animations
  • Added flutter_map_cancellable_tile_provider for better tile loading management
  • Updated MarkerLayer to use AnimatedMarkerLayer for improved performance
packages/flet_map/pubspec.yaml
packages/flet_map/lib/src/marker_layer.dart
packages/flet_map/lib/src/tile_layer.dart
Improved error handling and type safety
  • Enhanced error messages to include the specific control type
  • Improved null handling in various map utility functions
  • Updated type hints for event callbacks
sdk/python/packages/flet-core/src/flet_core/page.py
sdk/python/packages/flet-core/src/flet_core/types.py
packages/flet_map/lib/src/utils/map.dart
Refactored and simplified various map layer controls
  • Removed unnecessary 'children' parameter from several layer controls
  • Updated polygon and polyline layers to handle null values in coordinates
  • Simplified the creation of map controls in the createControl function
packages/flet_map/lib/src/circle_layer.dart
packages/flet_map/lib/src/polygon_layer.dart
packages/flet_map/lib/src/polyline_layer.dart
packages/flet_map/lib/src/create_control.dart

Tips
  • Trigger a new Sourcery review by commenting @sourcery-ai review on the pull request.
  • Continue your discussion with Sourcery by replying directly to review comments.
  • You can change your review settings at any time by accessing your dashboard:
    • Enable or disable the Sourcery-generated pull request summary or reviewer's guide;
    • Change the review language;
  • You can always contact us if you have any questions or feedback.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ndonkoHenri - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment to tell me if it was helpful.

@FeodorFitsner
Copy link
Contributor

Please resolve conflict.

@FeodorFitsner FeodorFitsner merged commit a217d5e into main Oct 19, 2024
2 checks passed
@FeodorFitsner FeodorFitsner deleted the map-enhancement branch October 19, 2024 01:38
pengwon added a commit to pengwon/flet that referenced this pull request Oct 30, 2024
* main: (31 commits)
  Migrate `colors` and `icons` variables to Enums (flet-dev#4180)
  feat: expose more properties in Controls (flet-dev#4105)
  feat!: Refactor `Badge` Control to a Dataclass; create new `Control.badge` property (flet-dev#4077)
  fix: clicking on `CupertinoContextMenuAction` doesn't close context menu (flet-dev#3948)
  fix dropdown `max_menu_height` (flet-dev#3974)
  Fix undefined name "Future" --> asyncio.Future (flet-dev#4230)
  wrap ListTile in material widget (flet-dev#4206)
  Update CONTRIBUTING.md (flet-dev#4208)
  TextField: suffix_icon, prefix_icon and icon can be Control or str (flet-dev#4173)
  feat!: enhance `Map` control (flet-dev#3994)
  skip running flutter doctor on windows if no_rich_output is True (flet-dev#4108)
  add --pyinstaller-build-args to pack cli command (flet-dev#4187)
  fix/feat: make `SearchBar`'s view height adjustable; add new properties (flet-dev#4039)
  fix: prevent button `style` from being modified in `before_update()` (flet-dev#4181)
  fix: disabling filled buttons is not visually respected (flet-dev#4090)
  when `label` is set, use `MainAxisSize.min` for the `Row` (flet-dev#3998)
  fix: `NavigationBarDestination.disabled` has no visual effect (flet-dev#4073)
  fix autofill in CupertinoTextField (flet-dev#4103)
  Linechart: jsonDecode tooltip before displaying (flet-dev#4069)
  fixed bgcolor, color and elevation (flet-dev#4126)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enhance Map Control
3 participants