Render mapbox vector tiles inside react-leaflet? - react-leaflet

Is there a way to use vector tiles from react-leaflet?
I am aware of Leaflet.VectorGrid, but it is not written for react-leaflet?

For react-leaflet v2, export the MapBoxGLLayer component wrapped with HOC withLeaflet() to get it working.
Steps:
1.Install mapbox-gl-leaflet.
npm i mapbox-gl-leaflet
2.Add mapbox-gl js and css to index.html
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.css' rel='stylesheet' />
3.Add this component.
import L from "leaflet";
import {} from "mapbox-gl-leaflet";
import PropTypes from "prop-types";
import { GridLayer, withLeaflet } from "react-leaflet";
class MapBoxGLLayer extends GridLayer {
createLeafletElement(props) {
return L.mapboxGL(props);
}
}
/*
* Props are the options supported by Mapbox Map object
* Find options here:https://www.mapbox.com/mapbox-gl-js/api/#new-mapboxgl-map-options-
*/
MapBoxGLLayer.propTypes = {
accessToken: PropTypes.string.isRequired,
style: PropTypes.string
};
MapBoxGLLayer.defaultProps = {
style: "mapbox://styles/mapbox/streets-v9"
};
export default withLeaflet(MapBoxGLLayer);
4.Use the MapBoxGLLayer component.
class App extends Component {
state = {
center: [51.505, -0.091],
zoom: 13
};
render() {
return (
<div>
<Map center={this.state.center} zoom={this.state.zoom}>
<MapBoxGLLayer
accessToken={MAPBOX_ACCESS_TOKEN}
style="mapbox://styles/mapbox/streets-v9"
/>
</Map>
</div>
);
}
}
Find the working code here (Add your own mapbox token): https://codesandbox.io/s/ooypokn26y

There are some really nice vector tiles examples in this react-leaflet issue (mapbox-gl example reproduced below).
// #flow
import L from 'leaflet'
import {} from 'mapbox-gl-leaflet'
import {PropTypes} from 'react'
import { GridLayer } from 'react-leaflet'
export default class MapBoxGLLayer extends GridLayer {
static propTypes = {
opacity: PropTypes.number,
accessToken: PropTypes.string.isRequired,
style: PropTypes.string,
zIndex: PropTypes.number,
}
createLeafletElement(props: Object): Object {
return L.mapboxGL(props)
}
}
and the usage of the above component:
<Map>
<MapBoxGLLayer
url={url}
accessToken={MAPBOX_ACCESS_TOKEN}
style='https://style.example.com/style.json'
/>
</Map>
NOTE: you may also need to npm install mapbox-gl and import that library and assign into to the global window.mapboxgl = mapboxgl to avoid issues with mapboxgl being undefined.

You can create a custom component by extending the MapLayer component. You can see an example of how this is done in react-leaflet 1.0 in a project I contributed to here.

In case anyone finds this question and is wondering how to do this with MapLibre GL JS (FOSS fork of Mapbox GL JS) as the backend renderer, you can do this but it's not immediately obvious. The MapLibre equivalent plugin is actively maintained now while the Mapbox one is not.
Here is the component code (in TypeScript) for a MapLibre tile layer that you can use instead of TileLayer in your React Leaflet MapContainer:
import {
type LayerProps,
createElementObject,
createTileLayerComponent,
updateGridLayer,
withPane,
} from '#react-leaflet/core'
import L from 'leaflet'
import '#maplibre/maplibre-gl-leaflet'
export interface MapLibreTileLayerProps extends L.LeafletMaplibreGLOptions, LayerProps {
url: string,
attribution: string,
}
export const MapLibreTileLayer = createTileLayerComponent<
L.MaplibreGL,
MapLibreTileLayerProps
>(
function createTileLayer({ url, attribution, ...options }, context) {
const layer = L.maplibreGL({style: url, attribution: attribution, noWrap: true}, withPane(options, context))
return createElementObject(layer, context)
},
function updateTileLayer(layer, props, prevProps) {
updateGridLayer(layer, props, prevProps)
const { url, attribution } = props
if (url != null && url !== prevProps.url) {
layer.getMaplibreMap().setStyle(url)
}
if (attribution != null && attribution !== prevProps.attribution) {
layer.options.attribution = attribution
}
},
)
Full sample code lives in this repo on GitHub: https://github.com/stadiamaps/react-leaflet-demo

Related

Multiple Doughnut chart as markers over Openstreetmap

I'm in trouble while implementing the doughnut chart over OpenStreetMap. I'm using react-chartjs2 for the doughnut chart and react-leaflet for Openstreetmap. Like we use the location icon on different coordinates over the map but here I want to use a Doughnut graph over the map instead of the location icon.
I want to achieve something like this
As per the react-leaflet documentation, the Marker icon property accepts two types of icons that is icon strings like image URL and divIcon which can be some HTML elements but while I'm rendering react component it does not accept and not showing it.
Here you can check in codesandbox I have added code to make it easy to try
https://codesandbox.io/s/doughnut-chart-over-osm-map-1indvl?file=/src/App.js
For what I know marker Icons can only be static, I use a function to create my only markers based on icons and plain html. Will be hard to do that with a component in your case.
My icon render function
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
In your case I would try to cheese it and create blank markers and show the graph in popups instead and just force the popups to alway stay open.
EDIT: Added my custom Marker code below that have some nice options.
You can just use the defaultOpen option, and add the graph as a child component to the marker and it will show up in the popup. You can the change the styling of you liking to make it look like the graph is the marker.
import { LatLngLiteral } from "leaflet";
import React, { Children, ReactElement, useEffect, useRef } from "react";
import { Marker, Popup, useMap } from "react-leaflet";
import { MapPin } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
export interface LeafletMarkerProps {
position: LatLngLiteral;
flyToPosition?: boolean;
size?: number;
color?: string;
icon?: ReactElement;
defaultOpen?: boolean;
onOpen?: () => void;
children?: React.ReactNode;
markerType?: string;
zIndexOffset?: number;
}
const LeafletMarker: React.FC<LeafletMarkerProps> = ({
position,
flyToPosition = false,
children,
size = 30,
color,
defaultOpen = false,
onOpen,
icon = <MapPin size={size} color={color} />,
markerType,
zIndexOffset,
}) => {
const map = useMap();
const markerRef = useRef(null);
position && flyToPosition && map.flyTo(position);
const markerIcon = createLeafletIcon(icon, size, markerType); // Important to not get default styling
useEffect(() => {
if (defaultOpen) {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}
}, [defaultOpen, position.lat, position.lng]);
return (
<Marker
eventHandlers={{
popupopen: () => onOpen && onOpen(),
}}
ref={markerRef}
icon={markerIcon}
position={position}
zIndexOffset={zIndexOffset}
>
{/* autoPan important to not have jittering */}
<Popup autoPan={false}>{children}</Popup>
</Marker>
);
};
export default LeafletMarker;

How to import Lottie component?

Remix is prone to the following error when using import on top-level components TypeError: Cannot read properties of undefined (reading 'root').
So I've done as they recommend and have the following imports.server.tsx file.
export * from "lottie-react";
Then my component app.tsx looks exactly like this lottie example.
import React from "react";
import * as Lottie from "../imports.server";
import groovyWalkAnimation from "../../public/assets/102875-cinema-clap.json";
export default function App() {
return (
<>
<h1>lottie-react - Component</h1>
<Lottie animationData={groovyWalkAnimation} />;
</>
);
}
but I get the following error
JSX element type 'Lottie' does not have any construct or call
signatures.ts(2604)
Edit 1:
The following seems to have worked for imports:
imports.server.tsx
import Lottie from "lottie-react";
export default Lottie;
AppTry.tsx
import React from "react";
import Lottie from "../imports.server";
import groovyWalkAnimation from "../../public/assets/102875-cinema-clap.json";
export default function AppTry() {
// console.log(LottieModule);
return (
<>
<h1>lottie-react - Component</h1>
<Lottie animationData={groovyWalkAnimation}></Lottie>
</>
);
}
Now the various paramaters like "animationData" and "autoPlay" pop up on the Lottie component which I assume means the import is working? However I am now getting this error when rendering AppTry.tsx?
react.development.js:220 Warning: React.createElement: type is invalid
-- expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to
export your component from the file it's defined in, or you might have
mixed up default and named imports.
Check the render method of AppTry.
Edit 2:
import { useLottie } from "lottie-react";
import Lottie from "lottie-react";
import groovyWalkAnimation from "../../public/assets/102875-cinema-clap.json";
const Example = () => {
const options = {
animationData: groovyWalkAnimation,
loop: true,
autoplay: true,
};
const { View } = useLottie(options);
return View;
};
const Example1 = () => {
return <Lottie animationData={groovyWalkAnimation} />;
};
export const TopicOverview = () => {
return (
<div className="space-y-20">
<Example1></Example1>
<Example></Example>
</div>
);
};
Looks like it has to do with your way of importing Lottie.
Shouldn't you import Lottie like this?:
import Lottie from "lottie-react";
I also struggled to get this working in Remix.
You can do the lazy load import somewhere higher up in the tree too.
import type { LottiePlayer } from "#lottiefiles/lottie-player";
import { useEffect } from "react";
interface LottieCompProps {
src: LottiePlayer["src"];
style?: Partial<LottiePlayer["style"]>;
}
function LottieComp({ src, style = {} }: LottieCompProps): JSX.Element | null {
// NB: otherwise, will cause app to crash. see https://remix.run/docs/en/v1/guides/constraints#third-party-module-side-effects
useEffect(() => {
import("#lottiefiles/lottie-player");
},[]);
if (typeof document === "undefined") return null;
return (
//#ts-expect-error dynamic import
<lottie-player
autoplay
loop
mode="normal"
src={typeof src === "string" ? src : JSON.stringify(src)}
style={{
...{
width: "100%",
backgroundColor: "transparent",
},
...style,
}}
/>
);
}
export default LottieComp;
The issue was in my root.tsx, an ErrorBoundary() function that called an <UnexpectedErrors/> component.
This same component was being called in various slug.tsx files. For some reason remix did not like this.
Having two different <UnexpectedErrors/> and <UnexpectedErrors2/> components - one for the slug.tsx files and one for the index.tsx files fixed this.

Change the center of React-leaflet

I am trying to dynamically change the center of a map-container with data provided externally. I get the data as a string, and then parse it to get it as numbers instead. But when I enter lat to the const center, I get a NaN when trying use it.
import React from 'react'
import { useCasparData } from 'caspar-graphics'
import { useTimeline } from '#nxtedition/graphics-kit'
import './style.css'
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
import './leaflet.css'
export default function Lowerthird () {
const { text01, text02, text03, text04 } = useCasparData()
const lat = parseFloat(text01)
const zoom = 15
const center = [lat, 13.440222]
function onLoad(timeline) {
timeline
.add('start')
.from('.name', { x: -2000 }, 'start')
.from('.titel', { x: -1000 }, 'start')
}
function onStop(timeline) {
timeline
.reverse()
}
//useTimeline(onLoad, onStop)
return (
<MapContainer center={center} zoom={zoom} zoomControl={false}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}
export const previewData = {
text01: '59.392133',
text02: '13.440222',
text03: '15',
text04: '[59.392133, 13.440222]'
}
I have looked through several threads here, but I have not found a answer that solves this for me... I do realize that the map-container is immutable - I just can't seem to figure out how to update it or set a new center...
(Oh... I am a total noob to react/leaflet, I am just trying to find a simple way to use Openstreetmap as a overlay in our broadcasts (tv))
You can use useMap() from a child component.
And with map, you can use functions such as flyTo to move around on the map.
Sadly useMap needs to be used from the child scope of MapContainer, however, you can just create an invisible component and add as a child and provide it with the new position when you want it to move.
So something like this (I have not tested this code but I have done similar stuff with markers):
interface ChangeCenterProps {
position: { lat: number; lng: number };
}
const ChangeCenter: React.FC<ChangeCenterProps> = ({ position }) => {
const map = useMap();
map.flyTo(position);
return <></>;
};

Map Size when Testing React Leaflet

I'm trying to test a custom react-leaflet component. It grabs the map element and uses a filter to get the number of elements visible at the current zoom level:
const visible = allPoints.filter(p => map.getBounds().contains(p));
This code works fine in the application, but when I try to test with Jest & Enzyme, the filter always returns 0 elements, regardless of zoom level. Poking at it a bit, I discovered that map.getSize() always returns 0x0 and map.getBounds() returns the same point for the southwest and northeast corners, which explains why it can't really contain any other points.
Is this just a bug in Enzyme or Leaflet or is there something that I need to do to set the map size explicitly?
import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import { MapContext } from "../../../context";
import MarkersShown from "../markers-shown";
import { Map } from "react-leaflet";
import { mockParsed, mockCenter, mockData } from "./__data__";
describe("MarkersShown Tests", () => {
it("renders the total asset count", async () => {
const parsed = mockParsed;
const center = mockCenter;
let wrapper;
await act(async () => {
wrapper = mount(
<MapContext.Provider value={mockData}>
<Map zoom={parsed.z} center={center} style={{ height: "100px", width: "100px" }}>
<MarkersShown />
</Map>
</MapContext.Provider>
);
});
wrapper.update();
expect(wrapper.html()).toEqual(
expect.stringMatching("Markers Shown of 3")
);
});
});

Popup always open in the marker

Is there any way the popup always stays open and does not need to click on it to open?
Expected behaviour
Actual behavior
With the introduction of react-leaflet version 2 which brings breaking changes in regard of creating custom components, it is no longer supported to extend components via inheritance (refer this thread for a more details)
In fact React official documentation also recommends to use composition instead of inheritance:
At Facebook, we use React in thousands of components, and we haven’t
found any use cases where we would recommend creating component
inheritance hierarchies.
Props and composition give you all the flexibility you need to
customize a component’s look and behavior in an explicit and safe way.
Remember that components may accept arbitrary props, including
primitive values, React elements, or functions.
The following example demonstrates how to extend marker component in order to keep popup open once the marker is displayed:
const MyMarker = props => {
const initMarker = ref => {
if (ref) {
ref.leafletElement.openPopup()
}
}
return <Marker ref={initMarker} {...props}/>
}
Explanation:
get access to native leaflet marker object (leafletElement) and open popup via Marker.openPopup method
Here is a demo
What you can do is to make your own Marker class from the react-leaflet marker, and then call the leaflet function openPopup() on the leaflet object after it has been mounted.
// Create your own class, extending from the Marker class.
class ExtendedMarker extends Marker {
componentDidMount() {
// Call the Marker class componentDidMount (to make sure everything behaves as normal)
super.componentDidMount();
// Access the marker element and open the popup.
this.leafletElement.openPopup();
}
}
This will make the popup open once the component has been mounted, and will also behave like a normal popup afterwards, ie. on close/open.
I threw together this fiddle that shows the same code together with the basic example.
You can use permanent tooltips, or React provides refs for this type of thing... you can do this:
https://jsfiddle.net/jrcoq72t/121/
const React = window.React
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet
class SimpleExample extends React.Component {
constructor () {
super()
this.state = {
lat: 51.505,
lng: -0.09,
zoom: 13
}
}
openPopup (marker) {
if (marker && marker.leafletElement) {
window.setTimeout(() => {
marker.leafletElement.openPopup()
})
}
}
render () {
const position = [this.state.lat, this.state.lng]
return (
<Map center={position} zoom={this.state.zoom}>
<TileLayer attribution='© OpenStreetMap contributors' url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<Marker position={position} ref={this.openPopup}>
<Popup>
<span>
A pretty CSS3 popup. <br /> Easily customizable.
</span>
</Popup>
</Marker>
</Map>
)
}
}
window.ReactDOM.render(<SimpleExample />, document.getElementById('container'))
References:
https://reactjs.org/docs/refs-and-the-dom.html
React.js - access to component methods
Auto open markers popup on react-leaflet map
The above no longer works with react-leaflet version 3. In your custom marker component, to get a reference to the leaflet element you should now use useRef() and then open up the popup in useEffect() once the component is mounted.
const MyMarker = (props) => {
const leafletRef = useRef();
useEffect(() => {
leafletRef.current.openPopup();
},[])
return <Marker ref={leafletRef} {...props} />
}
For the new react-leaflet v4 you will need to do some changes
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
map.openPopup(popupRef);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
{data.title}
</Popup>
</Marker>
);
};
And then use MapContainer like this
const MapComponent = () => {
const [map, setMap] = useState(null);
return (
<div>
<MapContainer
ref={setMap}
center={[45.34416, 15.49005]}
zoom={15}
scrollWheelZoom={true}
>
<CustomMarker
isActive
map={map}
data={{
position: [45.34416, 15.49005],
title: "Text displayed in popup",
}}
/>
</MapContainer>
</div>
);
};
Edited
Notice that my old solution would try to open the popup every time it renders.
Found another solution that fit my needs to open it when the position changed. Notice that I look at position.lat, position.lng since it will think it always changes if you pass on the object.
And yes it is not perfect typescript but it is the best solution I could come up with.
const CustomMarker: React.FC<CustomMarkerProps> = ({ position, children }) => {
const map = useMap();
const markerRef = useRef(null);
useEffect(() => {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}, [position.lat, position.lng]);
return (
<Marker ref={markerRef} position={position}>
<Popup>{children}</Popup>
</Marker>
);
};
export default CustomMarker;
Old solution
Could not get it to work using useRef and useEffect. However, got it to work with calling openPopup() directly from the ref.
import { LatLngLiteral, Marker as LMarker } from "leaflet";
import React from "react";
import { Marker } from "react-leaflet";
export interface CustomMarkerProps {
position: LatLngLiteral;
open?: boolean;
}
const CustomMarker: React.FC<CustomMarkerProps> = ({
position,
open = false,
children,
}) => {
const initMarker = (ref: LMarker<any> | null) => {
if (ref && open) {
ref.openPopup();
}
};
return (
<Marker ref={initMarker} position={position}>
{children}
</Marker>
);
};
export default CustomMarker;