What I'd like to do:
I would like to have a leaflet layers control in my Rails 7 app.
What I get:
Leaflet works fine, but the control appears without the icon, like this:
and in the browser console I get this error:
GET http://my_ip/trial_models/undefined/layers.png 404 (Not Found)
and in my server log the error is presented like this:
Started GET "/trial_models/undefined/layers.png" for my_ip at 2022-03-30 06:50:43 +0000
ActionController::RoutingError (No route matches [GET] "/trial_models/undefined/layers.png"):
I've replaced the actual ip address with "my_ip". The 'undefined' is really there in the error messages.
How I did it:
First I created a Rails 7 app and imported leaflet like this:
./bin/importmap pin leaflet
./bin/importmap pin leaflet-css
I then created a TrialModel using standard rails g scaffold. I then added the following stimulus div in trail_models\show.html.erb :
<div data-controller="trialmap" data-trialmap-target="trial" style="height:600px" class="leaflet-container"></div>
I created \app\javascript\controllers\trialmap_controller.js with the following contents:
import { Controller } from "#hotwired/stimulus";
import "leaflet-css";
export default class extends Controller {
static targets = [ "trial" ]
connect(){
import("leaflet").then( L => {
this.map = L.map(this.trialTarget).setView([ 51.472814, 7.321673 ], 14);
var base_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap | SwmmGo',
transparency: true,
opacity: 0.5
}).addTo(this.map);
var landMap = L.tileLayer('http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png', {attribution: 'attributions'});
var layersControl = new L.Control.Layers({
"Street Map": base_map,
"Landscape": landMap
});
this.map.addControl(layersControl);
});
}
disconnect(){
this.map.remove()
}
}
What I've explored:
I tried downloading leaflet-css like this:
./bin/importmap pin leaflet-css --download
but same result.
I can see that there is a references background-image:url(images/layers.png); in the imported/ downloaded file vendor/javascript/leaflet-css.js. But there doesn't seem be any images folder anywhere like vendor/javascript/images/ or the like.
The stringify version of L.Control.prototype.options is just {"position":"topright"}.
I tried setting L.Control.prototype.options.iconUrl = "layers.png" (and "/layers.png") in the js controller and then placing a manually downloaded layers.png file in public. But this changed neither the results nor the error messages.
My questions:
How can I get the icon to show and the error to disappear? (a work-around would be ok too)
What is the "correct way" to handle assets and asset paths which css files reference when using importmap?
Any help would be much appreciated! Thanks!
I had similiar problems when using markers, my solution was not using leaflet-css and just using the leaflet CSS from CDN
I did like Luis - put the leaflet.css downloaded from https://leafletjs.com in vendor/assets/stylesheets and the images in public/images - that solved my issues with broken marker/layer images
Don't use leaflet-css, it bundles both the leaflet css and an ancient version (0.6.4) of the leaflet js code. It seems that the other answers here are on target - rails importmaps are for js only; if you are working with a third-party lib that has its own css assets you will need to handle those separately either through a CDN or by downloading them and placing them alongside your other css assets.
I am working on this project where I want to add relatively dynamic data (updating every 15minutes) to a mapbox-gl via a serverless solution. I followed this excellent guide on creating serverless vector tiles and have the base tiles working.
My question is now how would you achieve adding a geojson point layer to the map (hosted on a simple HTML page), using something like:
var map = new mapboxgl.Map({
container: 'map',
style: style: "https://{domain}/bright.json",
});
var url = 'https://{domain}/geojsonfile.json'
map.addSource('geojsonfile', {
'type': 'geojson',
'data': url
});
map.addLayer({
'id': 'geojsonfile',
'type': 'symbol',
'source': 'geojsonfile',
'layout': {
'icon-image': 'rocket-15'
}
});
In a serverless vector environment without using Tippecanoe to convert the json file into protobuf vector tiles and instead adding the layer direct to the map from the javascript file. The above js for addsource and addlayer come from this Mapbox guide.
I can get the above js working when I pass in a mapboxgl.accessToken instead of self-hosting; however that's as far as I have been able to get. The geojson file is hosted in a CORS enabled s3 bucket.
Is the issue with loading in the source or displaying the layer? I have also tried modifying the bright.json file to handle the source and layer so that I only have to replace the s3 file, however have had no luck.
Any and all help / suggestions are much appreciated.
After quite a lot of blind trial and error, finally figured out a solution. Turns out my original script was trying to load the layer to the map on-load. However, the tile base load is configured via the style and tile files pre-load.
Therefore, the js addSource needs to be added to the map post-load and then style on-load.
this.map.on('style.load', function () {
this.pastInitialLoad = true;
this.map.addSource("geojsonfile", {
"type": "geojson",
"data": url }
);
If anyone else is stuck, these guides / sources were of great help!
https://schwanksta.com/blog/vector-tiles-introduction-pt1
http://fuzzytolerance.info/blog/2016/03/16/Leaflet-to-Mapbox-GL/
https://github.com/mapbox/mapbox-gl-js/issues/2268
I am trying to set up a system that loads a KMZ file and displays it using google maps and then tests if the user has clicked within the bounds of a polygon created by the KMZ. I have searched all through these forums and the web and haven't found a working solution.
I am having no problem loading and reading the KMZ file and the polygon is displaying perfectly on the map. But when I try to use the polygon data returned by geoXML3 to test if a location is within the bounds then I get a variety of errors depending on how I approach it.
I am loading the geoxml3.js files locally and the parser for the KMZ files, and as I say that works fine so I won't include all that.
The KMZ file is local to the server and reads fine.
This is what I have:
<script>
var zone;
var polzone;
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 11,
center: {lat: 0, lng: 0}
});
zone = new geoXML3.parser({map: map});
zone.parse('test.kmz');
}
function testlimit(){
polzone = zone.docs[0].gpolygons[0];
console.log( google.maps.geometry.poly.containsLocation(new google.maps.LatLng("0", "0"), polzone));
}
</script>
As you can see I'm testing just with hard coded long and lattitude values and it's giving me the error "Uncaught TypeError: b.get is not a function".
The testlimit function is fired when a button is clicked.
I initially had more code but as it wasn't working, this is where I've ended up. Any help would be appreciated.
Alright, never mind then, Apparently the gpolygon variable is an array.
So it should be
polzone = zone.docs[0].gpolygons[0];
One of those days I should have stayed in bed I guess...
I am ready with a port from a php mvc application to asp.net mvc (using a lot of javascript and google maps). The application works quite well on my development machine (Win 7 + IIS 7). But the problems start on production server. for some reason i have a javascript file wich is not working well when i am using it from my production server.
i.e. I have my view where i load all the data from db and and after that I put this on an global variable array called global_sites_am. Each row in the array contains attributes like latitud ,longitud and name.... After that this array is readed from the js file called maps.js.
var position = new google.maps.LatLng(global_sites_am[i].latitud,global_sites_am[i].longitud);
addMarker(position,global_sites_am[i].name,i);
For some reason this piece of code work fine on my development machine but it doesn't when i have installed the app on production server.
I.E. On Development machine : global_sites_am[0].latitud = 45,6789566 and global_sites_am[i].longitud=72,69452015
But on Production machine : global_sites_am[0].latitud = 45 and global_sites_am[i].longitud=72
What i am doing wrong?
Update: Here is How I load the value from the db to the javascript file:
global_sites_am[count]=new Object();
global_sites_am[count].name='<%=site.Name%>';
global_sites_am[count].latitud=<%=site.Latitud%>;
global_sites_am[count].longitud=<%=site.Longitud%>;
This is one of my properties:
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.Double Latitud
{
get
{
return _Latitud;
}
set
{
OnLatitudChanging(value);
ReportPropertyChanging("Latitud");
_Latitud = StructuralObject.SetValidValue(value);
ReportPropertyChanged("Latitud");
OnLatitudChanged();
}
}
Some debug info from Chrome: http://oi49.tinypic.com/2j4q7p4.jpg. In the image you will see that the values at the moment to load in the page are correct, but at the moment to read them are wrong.
Finally the problem was solved using the next line on the web.config:
globalization uiCulture="en-US" culture="en-US"
The Google Maps JavaScript version 3 API library documentation clearly explains:
The Google Maps API supports the KML
and GeoRSS data formats for displaying
geographic information. These data
formats are displayed on a map using a
KmlLayer object, whose constructor
takes the URL of a publicly accessible
KML or GeoRSS file.
There are even several Stack Overflow questions about how to load local data:
Loading a local .kml file using google maps?
Google Maps kml files
Some of the answers have pointed to third party libraries which can parse KML locally without the file needing to be public:
geoxml3
GeoXML
EGeoXml
And while these solutions are good if you have a need to keep your data private, I simply want to make development easier. When running locally I obviously cannot parse my KML and therefore lose functionality that I am trying to test. I've posted a single generic KML file on a publicly available site, but then have to have different development code to render one thing vs. something else when running for real.
What are my options for local development to render what would be publicly available dynamically generated KML files?
It seems like you've outlined the options pretty well:
If you want to work with local data, without involving a publicly accessible webserver, you'll need to use a javascript-based approach to parse the KML and load it onto the map. While this won't perfectly replicate the Google functionality, it is likely good enough for initial development if you only care about displaying the KML features. In this case, I'd probably set up a stub class, like this:
// I'll assume you have a global namespace called MyProject
MyProject.LOCAL_KML = true;
MyProject.KmlLayer = function(url) {
// parse the KML, maybe caching an array of markers or polygons,
// using one of the libraries you list in your question
};
// now stub out the methods you care about, based on
// http://code.google.com/apis/maps/documentation/javascript/reference.html#KmlLayer
MyProject.KmlLayer.prototype.setMap = function(map) {
// add the markers and polygons to the map, or remove them if !map
}
// etc
Now either put a switch in the code, or comment/uncomment, or use a build script to switch, or whatever your current process is for switching between dev and production code:
var kmlPath = "/my.kml";
var kmlLayer = MyProject.LOCAL_KML ?
new MyProject.KmlLayer(MyProject.LOCAL_KML_HOST + kmlPath) :
new google.maps.KmlLayer(MyProject.PRODUCTION_KML_HOST + kmlPath);
kmlLayer.setMap(myMap);
If, on the other hand, you need all of the functionality in the Google KmlLayer, or you want to make sure things work with the production setup, or you don't want to bother stubbing out the functionality Google provides, then you'll need to upload it to a publicly available server, so that Google can do its server-side processing.
Aside from the obvious options (FTP, a command-line script to upload your new KML file, etc), most of which require you to do something manually before you load your map page, you might consider building the update into the page you're loading. Depending on the platform you're using, this might be easier to do on the back-end or the front-end; the key would be to have a script on your public server that would allow the KML to be updated:
Get KML string from request.POST
Validate the KML string (just so you aren't opening your server to attacks)
Write to a single file, e.g. "my.kml"
Then, when you view your map page, update the remote KML based on the data from localhost. Here's a client-side version, using jQuery:
// again, you'd probably have a way to kill this block in production
if (MyProject.UPDATE_KML_FROM_LOCALHOST) {
// get localhost KML
$.get(MyProject.LOCAL_KML_HOST + kmlPath, function(data) {
// now post it to the remote server
$.post(
MyProject.DEV_KML_HOST + '/update_kml.php',
{ kml: data },
function() {
// after the post completes, get the KML layer from Google
var kmlLayer new google.maps.KmlLayer(MyProject.DEV_KML_HOST + kmlPath);
kmlLayer.setMap(myMap);
}
);
})
}
Admittedly, there are a lot of round-trips here (page -> localhost, page -> remote server, Google -> remote server, Google -> page), so this is going to be slow. But it would allow you to have Google's code properly render dynamic KML data produced on localhost, without having to take a separate manual step every time you reload the page.
Definitely, Google Maps KmlLayer is designed for you to send your data to them.
https://developers.google.com/maps/documentation/javascript/kml
Have a look the following log.
//console
var src = 'https://developers.google.com/maps/documentation/javascript/examples/kml/westcampus.kml';
var kmlLayer = new google.maps.KmlLayer(src, {
suppressInfoWindows: true,
preserveViewport: false,
map: your_gmap_object
});
Creating Marker, Polygon, they are all browser side parsing and rendering.
As you can see from next network log, KmlLayer class send source URL to Google Server to parse it and (do something in their end) and send the parsed result back to your browser to render.
//REQUEST from browser
https://maps.googleapis.com/maps/api/js/KmlOverlayService.GetOverlays?1shttps%3A%2F%2Fdevelopers.google.com%2Fmaps%2Fdocumentation%2Fjavascript%2Fexamples%2Fkml%2Fwestcampus.kml&callback=_xdc_._lidt3k&key=AIzaSyBeLTP20qMgxsQFz1mwLlzNuhrS5xD_a_U&token=103685
//RESPONSE from google server
/**/_xdc_._lidt3k && _xdc_._lidt3k( [0,"kml:cXOw0bjKUSmlnTN2l67v0Sai6WfXhSSWuyNaDD0mAzh6xfi2fYnBo78Y2Eg","|ks:;dc:tg;ts:51385071|kv:3|api:3",...
["KmlFile"],[[37.423017,-122.0927],[37.424194,-122.091498]],[["g74cf1503d602f2e5"],["g58e8cf8fd6da8d29"],["ge39d22e72437b02e"]],1,[["client","2"]],-21505,[["ks",";dc:tg;ts:51385071"],["kv","3"],["api","3"]]] )
As #capdragon mentioned above, it would be better parse KML by yourself.
UPDATE
Here is compact KML parser code.
This only for google.maps Marker and Polygon.
html
<input type='file' accept=".kml,.kmz" onchange="fileChanged()">
script, I used typescript but it is pretty same with javascript
file: any
fileChanged(e) {
this.file = e.target.files[0]
this.parseDocument(this.file)
}
parseDocument(file) {
let fileReader = new FileReader()
fileReader.onload = async (e: any) => {
let result = await this.extractGoogleCoords(e.target.result)
//CREATE MARKER OR POLYGON WITH result here
console.log(result)
}
fileReader.readAsText(file)
}
async extractGoogleCoords(plainText) {
let parser = new DOMParser()
let xmlDoc = parser.parseFromString(plainText, "text/xml")
let googlePolygons = []
let googleMarkers = []
if (xmlDoc.documentElement.nodeName == "kml") {
for (const item of xmlDoc.getElementsByTagName('Placemark') as any) {
let placeMarkName = item.getElementsByTagName('name')[0].childNodes[0].nodeValue.trim()
let polygons = item.getElementsByTagName('Polygon')
let markers = item.getElementsByTagName('Point')
/** POLYGONS PARSE **/
for (const polygon of polygons) {
let coords = polygon.getElementsByTagName('coordinates')[0].childNodes[0].nodeValue.trim()
let points = coords.split(" ")
let googlePolygonsPaths = []
for (const point of points) {
let coord = point.split(",")
googlePolygonsPaths.push({ lat: +coord[1], lng: +coord[0] })
}
googlePolygons.push(googlePolygonsPaths)
}
/** MARKER PARSE **/
for (const marker of markers) {
var coords = marker.getElementsByTagName('coordinates')[0].childNodes[0].nodeValue.trim()
let coord = coords.split(",")
googleMarkers.push({ lat: +coord[1], lng: +coord[0] })
}
}
} else {
throw "error while parsing"
}
return { markers: googleMarkers, polygons: googlePolygons }
}
output
markers: Array(3)
0: {lat: 37.42390182131783, lng: -122.0914977709329}
...
polygons: Array(1)
0: Array(88)
0: {lat: -37.79825999283025, lng: 144.9165994157198}
...