reflex-dev / reflex

πŸ•ΈοΈ Web apps in pure Python 🐍
https://reflex.dev
Apache License 2.0
19.15k stars 1.08k forks source link

Feature Request: Is there a way to add Open Street Map component and plot coordinates or polylines on them? #1291

Open mister-rao opened 1 year ago

corv89 commented 1 year ago

Hi, I'd also like to find a way to do this.

I tried wrapping "React Leaflet" but I'm stuck with my limited knowledge of Reflex. Perhaps someone can help out?

In rxconfig.py I added: frontend_packages=["react-leaflet", "leaflet"] to config

Then I add a class to my app.py:

class MapContainer(rx.Component):
    library = "react-leaflet"
    tag = "MapContainer"
    zoom: rx.Var[int]

Next, I create the map_container component:

map_container = MapContainer.create

Similarly I add another class and create another component:

class TileLayer(rx.Component):
    library = "react-leaflet"
    tag = "TileLayer"
    url: rx.Var[str]

tile_layer = TileLayer.create

I then add both components to my index page:

def index() -> rx.Component:
    return map_container(tile_layer(url=""), zoom=13)

Unfortunately this blows up and I get

Server Error
ReferenceError: window is not defined

Here's more information about React Leaflet

Thanks to iameli for helping out in DIscord

Alek99 pointed out that the undefined window error can be solved as it was done with Plotly

corv89 commented 1 year ago

I've written a working NextJS version of react-leaflet to know what to look for:

npm install leaflet react-leaflet

In pages/index.js:

import dynamic from "next/dynamic";
import "leaflet/dist/leaflet.css";

const MapComponent = dynamic(
  () => {
    return import("react-leaflet").then(({ MapContainer, TileLayer }) => {
      return () => (
        <MapContainer
          center={[51.505, -0.09]}
          zoom={13}
          scrollWheelZoom={true}
          style={{ height: "100vh", width: "100%" }}
        >
          <TileLayer
            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          />
        </MapContainer>
      );
    });
  },
  { ssr: false }
);

export default function Home() {
  return <MapComponent />;
}

Once Reflex emits the equivalent code it should work πŸ™

corv89 commented 1 year ago

@mister-rao We got it working thanks to the Reflex team in Discord! Special thanks to @Alek99 for doing the heavy lifting!

"""React-leaflet component in Reflex."""
import reflex as rx
from reflex.style import Style

class State(rx.State):
    """The app state."""

    pass

class LeafletLib(rx.Component):
    def _get_imports(self):
        return {}

    @classmethod
    def create(cls, *children, **props):
        custom_style = props.pop("style", {})

        # Transfer style props to the custom style prop.
        for key, value in props.items():
            if key not in cls.get_fields():
                custom_style[key] = value

        # Create the component.
        return super().create(
            *children,
            **props,
            custom_style=Style(custom_style),
        )

    def _add_style(self, style):
        self.custom_style = self.custom_style or {}
        self.custom_style.update(style)  # type: ignore

    def _render(self):
        out = super()._render()
        return out.add_props(style=self.custom_style).remove_props("custom_style")

class MapContainer(LeafletLib):
    library = "react-leaflet"
    tag = "MapContainer"

    center: rx.Var[list[float]]
    zoom: rx.Var[int]
    scroll_wheel_zoom: rx.Var[bool]

    def _get_custom_code(self) -> str:
        return """import "leaflet/dist/leaflet.css";
import dynamic from 'next/dynamic'
const MapContainer = dynamic(() => import('react-leaflet').then((mod) => mod.MapContainer), { ssr: false });
"""

class TileLayer(LeafletLib):
    library = "react-leaflet"
    tag = "TileLayer"

    def _get_custom_code(self) -> str:
        return """const TileLayer = dynamic(() => import('react-leaflet').then((mod) => mod.TileLayer), { ssr: false });"""

    attribution: rx.Var[str]
    url: rx.Var[str]

class UseMap(LeafletLib):
    library = "react-leaflet"
    tag = "useMap"

class Marker(LeafletLib):
    library = "react-leaflet"
    tag = "Marker"

    def _get_custom_code(self) -> str:
        return """const Marker = dynamic(() => import('react-leaflet').then((mod) => mod.Marker), { ssr: false });"""

    position: rx.Var[list[float]]
    icon: rx.Var[dict]

class Popup(LeafletLib):
    library = "react-leaflet"
    tag = "Popup"

    def _get_custom_code(self) -> str:
        return """const Popup = dynamic(() => import('react-leaflet').then((mod) => mod.Popup), { ssr: false });"""

map_container = MapContainer.create
tile_layer = TileLayer.create
use_map = UseMap.create
marker = Marker.create
popup = Popup.create

def index():
    return rx.center(
        map_container(
            tile_layer(
                attribution="&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
            ),
            marker(popup("Hello, world"), position=[51.505, -0.09]),
            center=[51.505, -0.09],
            zoom=13,
            scroll_wheel_zoom=True,
            height="98vh",
            width="100%",
        ),
    )

# Add state and page to the app.
app = rx.App(state=State)
app.add_page(index)
app.compile()

Note: marker-icon.png, marker-icon-2x.png and marker-shadow.png need to be in /assets for it to render correctly

Lendemor commented 5 months ago

Changed this to custom component request as this seems like a better fit.

related to #2855