Svelte / SvelteKit importing an npm library returns error when trying to work with Leaflet - 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;

Related

Svelte with leaflet

I'm trying to find my way into Svelte combined with leaflet. Where I'm stuck is how to correctly split the leaflet components into files. For learning, I'm trying to build the official official leaflet quickstart with svelte.
This is how my app.svelte looks like:
<script>
import L from 'leaflet';
import { onMount } from "svelte";
import { Circle } from "./components/Circle.svelte";
let map;
onMount(async () => {
map = L.map("map");
L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
attribution:
'Map data © OpenStreetMap contributors, CC-BY-SA',
maxZoom: 18,
tileSize: 512,
zoomOffset: -1
}).addTo(map);
map.setView([51.505, -0.09], 13);
Circle.addTo(map);
});
</script>
<style>
html,body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100vw;
}
</style>
<svelte:head>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin="" />
</svelte:head>
<div id="map" />
and my circle component:
<script context="module">
import L from 'leaflet';
export let map_obj;
export let Circle = L.circle([51.508, -0.11], {
color: "red",
fillColor: '#f03',
fillOpacity: 0.5,
radius: 500
});
</script>
While this is working I do not think it's effective to consider every component and add it to the map with Circle.addTo(map);. How could I pass in the map object to the circle component or is there some better pattern to build the map with several components?
Note: I do know of svelte/leaflet but like to start from scratch for learning.
This seemingly easy task is complicated due to the not-really-straightforward lifecycle of frameworks like Svelte, and the really-straightforward let-me-do-DOM-stuff architecture of Leaflet.
There are several approaches to this. I'll describe one, based on nesting Svelte components for Leaflet layers inside a Svelte component for a Leaflet map, and using setContext and getContext to handle the Leaflet L.Map instance around. (I'm borrowing this technique from https://github.com/beyonk-adventures/svelte-mapbox )
So a Svelte component for a L.Marker would look like:
<script>
import L from 'leaflet';
import { getContext } from "svelte";
export let lat = 0;
export let lng = 0;
let map = getContext('leafletMapInstance');
L.marker([lat, lng]).addTo(map);
</script>
Easy enough - get the L.Map instance from the Svelte context via getContext, instantiate the L.Marker, add it. This means that there must be a Svelte component for the map setting the context, which will need the components for the markers slotted in, i.e.
<script>
import LeafletMap from './LeafletMap.svelte'
import LeafletMarker from './LeafletMarker.svelte'
</script>
<LeafletMap>
<LeafletMarker lat=40 lng=-3></LeafletMarker>
<LeafletMarker lat=60 lng=10></LeafletMarker>
</LeafletMap>
...and then the Svelte component for the Leaflet map will create the L.Map instance, set it as the context, and be done, right? Not so fast. This is where things get weird.
Because of how Svelte lifecycle works, children components will get "rendered" before parent components, but the parent component needs a DOM element to create the L.Map instance (i.e. the map container). So this could get delayed until the onRender Svelte lifecycle callback, but that would happen after the slotted children get instantiated and their onRender lifecycle callbacks are called. So waiting for Svelte to instantiate a DOM element to contain the map and then instantiate the L.Map and then pass that instance to the context and then getting the context in the marker elements can be quite a nightmare.
So instead, an approach to this is to create a detached DOM element, instantiate a L.Map there, i.e. ...
let map = L.map(L.DomUtil.create('div')
...set it in the context, i.e. ...
import { setContext } from "svelte";
setContext('leafletMapInstance', map);
...this will allow Leaflet layers instantiated by the slotted components to be added to a detached (and thus invisible) map. And once all the lifecycle stuff lets the Svelte component for the L.Map have an actual DOM element attached to the DOM, attach the map container to it, i.e. have this in the HTML section of the Svelte component...
<div class='map' bind:this={mapContainer}>
...and once it's actually attached to the DOM, attach the map container to it and set its size, i.e. ...
let mapContainer;
onMount(function() {
mapContainer.appendChild(map.getContainer());
map.getContainer().style.width = '100%';
map.getContainer().style.height = '100%';
map.invalidateSize();
});
So the entire Svelte component for this Leaflet L.Map would look more or less like...
<script>
import L from "leaflet";
import { setContext, onMount } from "svelte";
let mapContainer;
let map = L.map(L.DomUtil.create("div"), {
center: [0, 0],
zoom: 0,
});
setContext("leafletMapInstance", map);
console.log("map", map);
L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
attribution:
'Map data © OpenStreetMap contributors, CC-BY-SA',
}).addTo(map);
onMount(() => {
mapContainer.appendChild(map.getContainer());
map.getContainer().style.width = "100%";
map.getContainer().style.height = "100%";
map.invalidateSize();
});
</script>
<svelte:head>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""
/>
</svelte:head>
<style>
.map {
height: 100vh;
width: 100vw;
}
</style>
<div class="map" bind:this="{mapContainer}">
<slot></slot>
</div>
See a working example here.
As a side note, I'll say that one should think it twice before sandwiching Leaflet in another JS framework, and think twice about the architecture for this (slotted components seem the cleanest and most extensible, but maybe a big data structure and some imperative programming for the Leaflet bits would be simpler). Sometimes, making sense of the lifecycle implications of more than one framework working at once can be very confusing, and very time-consuming when bugs appear.

how to create a jsfiddle using leaflet

I am struggling with jsfiddle trying to create a running example which uses leaflet.
because I was not successful I searched for some examples and found the following one working:
http://jsfiddle.net/kedar2a/LnzN2/2/
I then copied the example in a new fiddle
https://jsfiddle.net/aLn3ut5z/1/
but it is still not working...
when inserting the external resources, there was the following error:
jsfiddle.net says:
You're loading resources over HTTP not HTTPS, your fiddle will not
work. Do you wish to continue?
any suggestions what is wrong here?
p.s.: below is the code of the jsfiddle windows:
HTML:
<div id="map"></div>
CSS:
#map {
height: 500px;
width: 80%;
}
JAVASCRIPT:
// We’ll add a tile layer to add to our map, in this case it’s a OSM tile layer.
// Creating a tile layer usually involves setting the URL template for the tile images
var osmUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
osmAttrib = '© OpenStreetMap contributors',
osm = L.tileLayer(osmUrl, {
maxZoom: 18,
attribution: osmAttrib
});
// initialize the map on the "map" div with a given center and zoom
var map = L.map('map').setView([19.04469, 72.9258], 12).addLayer(osm);
// Script for adding marker on map click
function onMapClick(e) {
var marker = L.marker(e.latlng, {
draggable: true,
title: "Resource location",
alt: "Resource Location",
riseOnHover: true
}).addTo(map)
.bindPopup(e.latlng.toString()).openPopup();
// Update marker on changing it's position
marker.on("dragend", function(ev) {
var chagedPos = ev.target.getLatLng();
this.bindPopup(chagedPos.toString()).openPopup();
});
}
map.on('click', onMapClick);
The Leaflet CDN doesn't support SSL yet. You can use something not requiring https, like playground-leaflet which is just a fork of JSBin with leaflet libraries easily selectable.
Alternatively, you could use Leaflet from cdnjs.net, which does support https.

Leaflet wont work with Vue using browserify

I'm new using browserify and VueJS. I have a project where I'd like to use the map service Leaflet. So as i show in the code down below I try to require it, I have pulled it in using npm. If I look in the compiled JS file i can find Leaflet there. So that seems to work..
I'll show a image of my resulting map when i try to initialize it in the browser:
Broken Leaflet map
So as you can see the tiles are behaving really wierd, and nothing works as it should. Draging the map does really strange things..
This is the code for the components JS.
var L = require('leaflet');
module.exports = {
data: function () {
return {
map: false,
samples: [],
messages: [],
customer_id: '',
year: ''
}
},
methods: {
fetch: function (id, year, successHandler) {
var that = this
that.$set('customer_id', id)
that.$set('year', year)
successHandler(id);
}
},
ready: function () {
var map = L.map('map').setView([41.3921, 2.1705], 13);
L.Icon.Default.imagePath = 'images/';
var osmTiles = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
var attribution = '© OpenStreetMap contributors';
L.tileLayer(osmTiles, {
maxZoom: 18,
attribution: attribution
}).addTo(map);
},
route: {
data: function (transition) {
this.fetch(this.$route.params.customer_id, this.$route.params.year, function (data) {
transition.next({customer_id: data})
})
}
}}
Right now I tried require:ing the leaflet package in my components script file.
Somewhere along the line Leaflet breaks.. I get no errors in the console when viewing the map, but as you can see it doesn't look good.
I would be really happy for all the help i can get!
Edit:
I just tried using chart.js in my project, required in the same way as leaflet, works like a charm.. Can't understand why leaflet wont work.
/ Simon
You are missing the Leaflet style sheet. Add it (for the correct version) using a link tag:
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" />

Leaflet Map Container already initialized

I'm creating an application with CartoDB that uses a 'map' object from Leaflet. Every time I run my app, the console runs the error: Uncaught Error: Map container is already initialized.
Here is the JS code. I've tried different approaches for hours, but nothing works. I would really appreciate some help here.
$( document ).ready(function(){
var mymap = L.map('map_div', {
zoomControl: true,
center: [40.4000, -3.7167],
zoom: 6
});
var basemap = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors, © CartoDB'
}).addTo(mymap);
});
//Please note that I have a "map_div" in my html and css
Thanks!

leaflet loads incomplete map

I am dynamically adding a leaflet map to a div. The map is only shown partially, see picture. Once I resize the browser window the map is completely shown. Is there a way around this? I tried some initial ideas, see // in code. But none of them worked.incomplete map
function mapThis(lat,long,wikiTitle){
var div_Id= "#wikiExtract"+wikiTitle
var map_Id= "map"+wikiTitle
$(div_Id).append("<div id='"+map_Id+"' style='width: 600px; height:
400px'></div>");
var map = L.map(map_Id).setView([lat, long], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-
SA</a>'
}).addTo(map);
//$("#"+map_Id).css( "height", "500px" );
//map.invalidateSize()
//$("#"+map_Id).hide()
//$("#"+map_Id).show()
//$(window).trigger('resize');
}
you can use this code :
it work find for me.
setInterval(function () {
map.invalidateSize();
}, 100);
If at least you have the map working (except for the incompleteness of tiles loading), you could simply call map.invalidateSize() once your map's div container is revealed / inserted into DOM with final size.
Checks if the map container size changed and updates the map if so — call it after you've changed the map size dynamically, also animating pan by default.
If using JQuery, the following code updates the size once the document is ready:
$(document).ready(function () {
mymap.invalidateSize();
});