Leaflet 1.7: L.MarkerClusterGroup is not a function - leaflet

I'm trying to use MarkerClusterGroup on a leaflet map. I have the error L.MarkerClusterGroup is not a function. I've read the related threads, but they are only valid for versions below leaflet 1.7.
I'm using React with webpack.
import { Icon, Marker, Circle, LatLngBounds, Popup, DivIcon } from "leaflet";
import "leaflet.markercluster";
const divIcon = new DivIcon();
const markersCluster = L.MarkerClusterGroup({
chunkedLoading: true,
iconCreateFunction: function (cluster) {
return divIcon({
html: cluster.getChildCount(),
className: "mycluster",
iconSize: null,
});
},
});
I've also tried to import L globally:
import * as L from "leaflet";
import "leaflet.markercluster";
const divIcon = new L.DivIcon();
const markersCluster = L.MarkerClusterGroup({
chunkedLoading: true,
iconCreateFunction: function (cluster) {
return divIcon({
html: cluster.getChildCount(),
className: "mycluster",
iconSize: null,
});
},
});
How to fix this?

Depending on your build engine, the imported L namespace from "leaflet" may not be augmented with MarkerClusterGroup (from leaflet.markercluster plugin) unfortunately.
But you can resort to using window.L instead, which is always augmented by the plugin.
BTW, either use the class constructor form with new keyword: new window.L.MarkerClusterGroup(), or use the factory form with lowerCamelCase: L.markerClusterGroup()
import * as L from "leaflet";
import "leaflet.markercluster";
console.log(window.L === L); // false...
const divIcon = new L.DivIcon();
const markersCluster = new window.L.MarkerClusterGroup({ // Use window.L for plugins
chunkedLoading: true,
iconCreateFunction: function (cluster) {
return divIcon({
html: cluster.getChildCount(),
className: "mycluster",
iconSize: null,
});
},
});
Live demo: https://stackblitz.com/edit/js-ojki89?file=index.js

This error raised on my app after upgrade leaflet version to 1.9.1 from 1.8.0
Window.L.markerClusterGroup() instead of L.markerClusterGroup() works for me.

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 update Leaflet.markercluster Icon on Event `spiderfied`

I'm using the Leaflet.markercluster plugin (https://github.com/Leaflet/Leaflet.markercluster) and
I'm struggling to update the markerClusterGroup Icon when the event spiderfied is fired.
Code:
// Initiate markers w/ a markerClusterGroup and the default icon
var markers = L.markerClusterGroup({
showCoverageOnHover: false,
});
// Set marker, bind it to Popup and add the markers as an layer to the map
markers.addLayer(L.marker(...))
.bindPopup(
L.popup({offset: L.point(0,0)}).setContent(
`...`
).openPopup()));
map.addLayer(markers);
// Try to update the default markerClusterGroup icon when a markerClusterGroup is spiderfied
markers.on('spiderfied', function (a) {
console.log('spiderfied');
markers.options = {
showCoverageOnHover: false,
iconCreateFunction: function() {
return L.divIcon({ html: '<b>' + 'Test' + '</b>' });
}
};
markers.refreshClusters();
});
What am I doing wrong here? Any advices?
Thanks in advance!
I've tried to get: https://github.com/Leaflet/Leaflet.markercluster#refreshing-the-clusters-icon implemented
I've done research on internet

Svelte / SvelteKit importing an npm library returns error when trying to work with Leaflet

I'm trying to learn Svelte / SvelteKit by porting over an existing Angular application. The app should show a Leaflet map with a heatmap layer as overlay. The map part works and is robust, even when I navigate or refresh Svelte handles it fine. The heatmap on the other hand only loads when the app initializes for the first time as you can see here:
However When I refresh I get this error and the whole Map.svelte component doesn't load at all anymore with the following error message in the console:
Uncaught (in promise) TypeError: Leaflet.heatLayer is not a function
I suspect it has to do with the way the lifecycle handles imports, because in my Angular app the imports don't have to be done in a life cycle method in order for them to work, whereas the only way to get Leaflet to even render in SvelteKit I have to do an async import.
Can anyone clarify what's going on with the Leaflet.heatlayer error and how I can fix it?
Map.svelte
<script lang="ts">
import type { HeatLayer, Map } from 'leaflet';
import { onMount } from 'svelte';
let Leaflet;
let map: Map;
let heatLayer: HeatLayer;
onMount(async () => {
Leaflet = await import('leaflet');
import('leaflet.heat');
const heatLatLngTuple = await fetchData(); // fetchData returns data from JSON file
const mapTiles = Leaflet.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
});
heatLayer = Leaflet.heatLayer(heatLatLngTuple, {. // THIS LINE IS CAUSING THE ERROR
radius: 20,
blur: 25,
minOpacity: 0,
maxZoom: 6,
max: 12
});
map = Leaflet.map('map', {
center: [51.505, -0.09],
zoom: 6,
layers: [mapTiles, heatLayer]
});
});
</script>
<div id="map" />
Things I've tried:
including 'leaflet-heat.js' from node_modules in a <script> tag in app.html
including 'leaflet-heat.js' from node_modules in a <script> tag in __layout.svelte
including 'leaflet-heat.js' from node_modules in a <script> tag in index.svelte
importing leaflet.heat at the top of Map.svelte with "import 'leaflet.heat'" <- THIS WORKED IN ANGULAR! but here it just results in this error
ReferenceError: window is not defined
putting a tick() before assigning heatLayer in Map.svelte
Resources:
My GitHub repo
Leaflet.heat
As this answer points out there is an additional way of importing that I didn't know about using
vite-plugin-iso-import
After getting that set up my component now works and after importing "leaflet.heat" with ?client added and moved to the top level of my imports. Here is the link to the FAQ with a detailed explanation.
After the changes my component now looks like this:
Map.svelte
<script lang="ts">
import type { HeatLayer, Map } from 'leaflet';
import { onMount } from 'svelte';
import Leaflet from 'leaflet?client'; // provides definition of 'L' needed by Leaflet
import 'leaflet.heat?client'; // Note the '?client' after the module name which makes sure 'leaflet.heat' always has access to the 'window' object
let map: Map;
let heatLayer: HeatLayer;
onMount(async () => {
const heatLatLngTuple = await fetchData();
const mapTiles = Leaflet.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
});
heatLayer = Leaflet.heatLayer(heatLatLngTuple, {. // THIS LINE IS CAUSING THE ERROR
radius: 20,
blur: 25,
minOpacity: 0,
maxZoom: 6,
max: 12
});
map = Leaflet.map('map', {
center: [51.505, -0.09],
zoom: 6,
layers: [mapTiles, heatLayer]
});
});
</script>
<div id="map" />
I had this same issue on refresh, you need import the module inside the on mount. It's not the same as your code. But you get that point.
onMount(async () => {
const leafletModule = await import('leaflet');
L = leafletModule.default;

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")
);
});
});

Render mapbox vector tiles inside 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