Jump to content

Module: Leaflet Map


Mats

Recommended Posts

Hi to everybody and special thanks to @Mats for sharing this big porting, to @gebeer @netcarver @dab for the hard implementations and  to @Ivan Gretsky to keep it alive again, but also to all the contributors and maintainers of this awesome module. 

I'm testing it and it works really fine and easy.

I would try to add an extra class to the markers and I followed this linked tip to Leaflet.AwesomeMarker properties for the main page with all the markers:

   $options = array(
        'useMarkerSettings' => true,
        'markerFormatter' => function($page, $marker_options) {
            $marker_options['icon'] = $page->icon; // Override the default icon for this marker.
            $marker_options['markerColor'] = $page->color; // Override the default color for this marker.
			// Adding extra classes to this marker ??
            $marker_options['extraClasses'] = 'music-stage'; // Or dynamically from an option field: $page->stage_kind->value
            return $marker_options;
        }, 
...
);

But as you can see from the screenshot below, no classes are added to the icon (there is a blank space instead between the classes into <i></i> tag; actually it's always present, with or without the 'extraClasses' index into $marker_options).

150269848_Schermata2023-06-29alle07_09_13.thumb.jpg.51c63adf3cb80514fa345c485f911983.jpg

Am I missing something?

P.S. The JS correctly loads  the object properties:

mleafletmap1.addMarkerIcon(L.AwesomeMarkers.icon({prefix: 'fa', icon: 'music', markerColor: 'red', iconColor: 'white', extraClasses: 'music-stage'})

Basically it would be nice to add extra classes to the whole marker (not only to the icon) to manipulate it with js (eg. filtering by type/class on a triggered event such as a button click or select change, or hiding from page if an event date is passed, like for exibitions/music lives and so on).

Does anybody have any suggestions to do this?

Thanks in advance.

Edited by Cybermano
typo fixed
Link to comment
Share on other sites

  • 2 weeks later...

Hi, is there anybody that knows how to easly disable "scrollWheelZoom" on admin page editing?

Or is there a "module/field" settings development plan for map options in admin?
(an overrideble setting in the field options could be great).


I find a little annoying the zooming in/out of the map on page scroll.

At the moment I put manually a js line of code in InputfieldLeafletMapMarker.js after line 24

// line 24
var map = L.map(mapElement).setView([lat, lng], options.zoom);

// new line
map.scrollWheelZoom.disable();

But I'm not so happy to modify original modules... (on updates the modifications could be lose).

The second approach that I have succesfully tested is to load a js file in admin, but it needs to completely initialize a new map, only with the add of an option in the declaration of the map init:

var map = L.map('_Inputfield_fm_mappa_map', {center: [lat, lng], zoom: parseInt($('#_Inputfield_fm_mappa_zoom').val()), scrollWheelZoom: 0,} );
Spoiler

Complete code

$(document).ready(function(){

    if ($('form#ProcessPageEdit').hasClass('YOUR_PW_TEMPLETE_NAME_TO_EDIT')) {

        var mapId = '_Inputfield_YOUR_FIELD_MAP_map';

        var lat = $('#_Inputfield_YOUR_FIELD_MAP_lat').val();
        var lng = $('#_Inputfield_YOUR_FIELD_MAP_lng').val();
        var zoom = parseInt($('#_Inputfield_YOUR_FIELD_MAP_zoom').val());

        var options = {
            zoom: 9,
            draggable: true,
            center: null,
            scrollWheelZoom: 0,
        }
        
        // var map = $('#_Inputfield_YOUR_FIELD_MAP_map');
        var map = L.map('_Inputfield_YOUR_FIELD_MAP_map', {center: [lat, lng], zoom: zoom, scrollWheelZoom: 0,} );

        var coder = L.Control.Geocoder.nominatim(),
        geocoder = L.Control.geocoder({
            geocoder: geocoder, placeholder: ''
        }).addTo(map);

        var marker = L.marker(
            [lat,lng],
            {draggable: options.draggable}
        ).addTo(map);

        // tiles provider according to module settings (not mandatory), like OpenStreetMap.Mapnik
        L.tileLayer.provider('YOUR_SELECTED_TILES_PROVIDER').addTo(map);

        var $map = $('#' + mapId);
        //var $latlng = $map.siblings(".InputfieldLeafletMapMarkerLatLng").find("input[type=text]");
        var $lat = $map.siblings(".InputfieldLeafletMapMarkerLat").find("input[type=text]");
        var $lng = $map.siblings(".InputfieldLeafletMapMarkerLng").find("input[type=text]");
        var $addr = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[type=text]");
        var $raw = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[name$=_raw]");
        var $zoom = $map.siblings(".InputfieldLeafletMapMarkerZoom").find("input[type=number]");
   

        $( ".InputfieldLeafletMapMarkerAddress" ).on( "click", function() {
            $(".leaflet-control-geocoder.leaflet-control").toggleClass("leaflet-control-geocoder-expanded");
            setTimeout(function() { $('input.undefined').focus() }, 300);
        });

        $lat.val(marker.getLatLng().lat);
        $lng.val(marker.getLatLng().lng);
        $zoom.val(map.getZoom());

        $zoom.change(function(event) {
            map.setZoom($zoom.val());
            map.setView(marker.getLatLng());
        });

        $lat.change(function(event) {
            marker.setLatLng([$lat.val(),$lng.val()]);
            map.setView(marker.getLatLng(), 9);

            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        $lng.change(function(event) {
            marker.setLatLng([$lat.val(),$lng.val()]);
            map.setView(marker.getLatLng(), 9);
            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        geocoder.markGeocode = function(result) {
            marker.setLatLng(result.center);
            map.fitBounds(result.bbox);
            $lat.val(result.center.lat);
            $lng.val(result.center.lng);
            $addr.val(result.name);
            $raw.val(JSON.stringify(result.properties));
        };

        marker.on('dragend', function(event) {
            var result = marker.getLatLng();
            $lat.val(result.lat).trigger('change.custom');
            $lng.val(result.lng).trigger('change.custom');

            //reverse geocoding displays in the adress field
            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        map.on('zoomend', function(event){
            $zoom.val(map.getZoom());
        });        

        // get the tab element where this map is integrated
        var $map = $('#' + mapId); 
        
        // Get closed wrappers around the map.
        var $inputFields = $map.parents('.Inputfield.InputfieldStateCollapsed');

        // Refresh the map when any of the wrappers open.
        $inputFields.on('opened',function(){
            window.setTimeout(function(){
                map.invalidateSize();
            }, 200);
        });

        function initializeLeafletMap() {
            $(".InputfieldLeafletMapMarkerMap").each(function(item) {
                var $t = $(this);
                if (!$t.children().length) {
                    InputfieldLeafletMapMarker.init($t.attr('id'), $t.attr('data-lat'), $t.attr('data-lng'), $t.attr('data-zoom'), $t.attr('data-type'), $t.attr('data-provider'));
                }
            });
        }
        
        $(document).ready(function() {
            initializeLeafletMap();
            $(document).on('reloaded', '.InputfieldLeafletMapMarker', initializeLeafletMap);
        }); 

    }

});

 

Any suggestions?

 

 

Link to comment
Share on other sites

23 hours ago, Cybermano said:

Hi, is there anybody that knows how to easly disable "scrollWheelZoom" on admin page editing?

Or is there a "module/field" settings development plan for map options in admin?
(an overrideble setting in the field options could be great).


I find a little annoying the zooming in/out of the map on page scroll.

At the moment I put manually a js line of code in InputfieldLeafletMapMarker.js after line 24

// line 24
var map = L.map(mapElement).setView([lat, lng], options.zoom);

// new line
map.scrollWheelZoom.disable();

But I'm not so happy to modify original modules... (on updates the modifications could be lose).

The second approach that I have succesfully tested is to load a js file in admin, but it needs to completely initialize a new map, only with the add of an option in the declaration of the map init:

var map = L.map('_Inputfield_fm_mappa_map', {center: [lat, lng], zoom: parseInt($('#_Inputfield_fm_mappa_zoom').val()), scrollWheelZoom: 0,} );
  Reveal hidden contents

Complete code

$(document).ready(function(){

    if ($('form#ProcessPageEdit').hasClass('YOUR_PW_TEMPLETE_NAME_TO_EDIT')) {

        var mapId = '_Inputfield_YOUR_FIELD_MAP_map';

        var lat = $('#_Inputfield_YOUR_FIELD_MAP_lat').val();
        var lng = $('#_Inputfield_YOUR_FIELD_MAP_lng').val();
        var zoom = parseInt($('#_Inputfield_YOUR_FIELD_MAP_zoom').val());

        var options = {
            zoom: 9,
            draggable: true,
            center: null,
            scrollWheelZoom: 0,
        }
        
        // var map = $('#_Inputfield_YOUR_FIELD_MAP_map');
        var map = L.map('_Inputfield_YOUR_FIELD_MAP_map', {center: [lat, lng], zoom: zoom, scrollWheelZoom: 0,} );

        var coder = L.Control.Geocoder.nominatim(),
        geocoder = L.Control.geocoder({
            geocoder: geocoder, placeholder: ''
        }).addTo(map);

        var marker = L.marker(
            [lat,lng],
            {draggable: options.draggable}
        ).addTo(map);

        // tiles provider according to module settings (not mandatory), like OpenStreetMap.Mapnik
        L.tileLayer.provider('YOUR_SELECTED_TILES_PROVIDER').addTo(map);

        var $map = $('#' + mapId);
        //var $latlng = $map.siblings(".InputfieldLeafletMapMarkerLatLng").find("input[type=text]");
        var $lat = $map.siblings(".InputfieldLeafletMapMarkerLat").find("input[type=text]");
        var $lng = $map.siblings(".InputfieldLeafletMapMarkerLng").find("input[type=text]");
        var $addr = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[type=text]");
        var $raw = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[name$=_raw]");
        var $zoom = $map.siblings(".InputfieldLeafletMapMarkerZoom").find("input[type=number]");
   

        $( ".InputfieldLeafletMapMarkerAddress" ).on( "click", function() {
            $(".leaflet-control-geocoder.leaflet-control").toggleClass("leaflet-control-geocoder-expanded");
            setTimeout(function() { $('input.undefined').focus() }, 300);
        });

        $lat.val(marker.getLatLng().lat);
        $lng.val(marker.getLatLng().lng);
        $zoom.val(map.getZoom());

        $zoom.change(function(event) {
            map.setZoom($zoom.val());
            map.setView(marker.getLatLng());
        });

        $lat.change(function(event) {
            marker.setLatLng([$lat.val(),$lng.val()]);
            map.setView(marker.getLatLng(), 9);

            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        $lng.change(function(event) {
            marker.setLatLng([$lat.val(),$lng.val()]);
            map.setView(marker.getLatLng(), 9);
            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        geocoder.markGeocode = function(result) {
            marker.setLatLng(result.center);
            map.fitBounds(result.bbox);
            $lat.val(result.center.lat);
            $lng.val(result.center.lng);
            $addr.val(result.name);
            $raw.val(JSON.stringify(result.properties));
        };

        marker.on('dragend', function(event) {
            var result = marker.getLatLng();
            $lat.val(result.lat).trigger('change.custom');
            $lng.val(result.lng).trigger('change.custom');

            //reverse geocoding displays in the adress field
            coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) {
                var r = results[0];
                if (r) {
                    $addr.val(r.name);
                    $raw.val(JSON.stringify(r.properties));
                }
            })
        });

        map.on('zoomend', function(event){
            $zoom.val(map.getZoom());
        });        

        // get the tab element where this map is integrated
        var $map = $('#' + mapId); 
        
        // Get closed wrappers around the map.
        var $inputFields = $map.parents('.Inputfield.InputfieldStateCollapsed');

        // Refresh the map when any of the wrappers open.
        $inputFields.on('opened',function(){
            window.setTimeout(function(){
                map.invalidateSize();
            }, 200);
        });

        function initializeLeafletMap() {
            $(".InputfieldLeafletMapMarkerMap").each(function(item) {
                var $t = $(this);
                if (!$t.children().length) {
                    InputfieldLeafletMapMarker.init($t.attr('id'), $t.attr('data-lat'), $t.attr('data-lng'), $t.attr('data-zoom'), $t.attr('data-type'), $t.attr('data-provider'));
                }
            });
        }
        
        $(document).ready(function() {
            initializeLeafletMap();
            $(document).on('reloaded', '.InputfieldLeafletMapMarker', initializeLeafletMap);
        }); 

    }

});

 

Any suggestions?

 

 

Let me encourage you to create a PR) I promise to work with it.

  • Thanks 2
Link to comment
Share on other sites

OHHH, SORRY! I've replyed from my co-worker account: I'm Cybermano.

---

Hi @Ivan Gretsky, as promised I made a PR on GitHub: hope it's ok.

Basically I added a new InputfieldCheckbox in the InputfieldLeafletMapMarker.module fields configuration.

Then I setted it in the constructor and also assigned to a variable into the ___render(): if the checkbox is checked, this variable will inoculate an html class ("scrollwheel-disabled") into the div.InputfieldLeafletMapMarkerMap.

At the end, into the InputfieldLeafletMapMarker.js, if the div has this class assigned, map.scrollWheelZoom.disable() will do the work.

 

Sorry, I didn't found a cleaner way ?: hope this could be helpful or of inspiration.?

I attach the two file here, too.

InputfieldLeafletMapMarker.js InputfieldLeafletMapMarker.module

Edited by Mike-it
Typos fixed
  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...

I had a problem with PHP 8.1.8 an saving pages containing the field. I got this error:

Quote

Fatal Error: Uncaught TypeError: round(): Argument #1 ($num) must be of type int|float, string given in site/modules/FieldtypeMapMarker/InputfieldLeafletMapMarker.module:193

I changed lines 193 and 194 to make sure all values that get rounded are floats:

 if( ((string) round(floatval($lat), $precision)) != ((string) round(floatval($this->defaultLat), $precision)) ||
     ((string) round(floatval($lng), $precision)) != ((string) round(floatval($this->defaultLng), $precision))) {

Maybe this could be included in a future version?

Cheers,
Flo

  • Like 2
Link to comment
Share on other sites

Hi there,

I'm testing a static call with JS to set a new view of the map (I need it on event change without refreshing the page), as found on Github and Stackoverflow:

// frontend
var mymap = L.map($('#mleafletmap1')).setView([45.53, 10.21], 2);

// backend
var mymap = L.map($('#_Inputfield_fm_mappa_map')).setView([45.53, 10.21], 2);

This produces a TypeError: t.className is undefined.

I looking for a solution, but didn't found it (...and I'm not a skilled js coder).

Does anybody have an idea?

Link to comment
Share on other sites

I would advice all you that the geocoder API is no longer accessible in the format of the current module.

I have an error geocoding address and the return message from nominatim.openstreetmap is: 
Using the URL /search/ and /reverse/ (with slashes) is no longer supported.
Change url from /search/?q=Berlin in /search?q=Berlin

Also complete addresses are changed, see below:

File not found: API no longer accessible via this URL

Using the URL /search/ and /reverse/ (with slashes) is no longer supported. Please use URLs as given in the documentation.

Examples how to change the URL:

You use: https://nominatim.openstreetmap.org/search/?q=Berlin
Change to: https://nominatim.openstreetmap.org/search?q=Berlin

You use: https://nominatim.openstreetmap.org/search/US/Texas/Huston
Change to: https://nominatim.openstreetmap.org/search?q=Huston, Texas, US

See github issue #3134 for more details.

See github issue #3134 for more details.

 

Maybe editing ControlGeocoder.js fixes partially the problem?

:343 L.Control.Geocoder.jsonp(this.options.serviceUrl + 'search/', L.extend({
change in
L.Control.Geocoder.jsonp(this.options.serviceUrl + 'search', L.extend({

:370 L.Control.Geocoder.jsonp(this.options.serviceUrl + 'reverse/', L.extend({
change in
L.Control.Geocoder.jsonp(this.options.serviceUrl + 'reverse', L.extend({

 

I have tested with simple addresses (italian street, number and city) ad it works fine again, both search and reverse.

Posted a commit on Github:
https://github.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/commit/63254dffea9bd504eb45b124d64fd5832e38f013

  • Like 4
  • Thanks 4
Link to comment
Share on other sites

  • 1 year later...

Hi,

does anybody know how to use the module with a repeater? I need to have multiple mappoints in one map at my frontend, and due to the nature of the site I cannot put them in pages. So I thought of using a repeater for those points (as it is not possible to add multiple markers to one map). In the backend it works fine, but I cannot figure out how to collect those points and display them in one map.

 

<?php
$map = $modules->get('MarkupLeafletMap');
echo $map->getLeafletMapHeaderLines();
$mappoints = [];
foreach ($page->map_repeater as $point) {
        array_push($mappoints, $point->map); //or 
		array_push($mappoints, [$point->map]); //or
		array_push($mappoints, $point->map->LeafletMapMarker);
		array_push($mappoints, [$point->map->lat, $point->map->lng]);
	}
echo $map->render($page, $mappoints, array('height' => '270px'));
?>

All I tried gave me different mistakes. Does anybody know the correct syntax?

Link to comment
Share on other sites

Hi @torf

Never used Leaflet Map with repeaters, but in past I had to collect lot of points into a single map.
 

I simply used a PageArray with wire('pages')-find('template=MY_TEMPLATE, and so on...') and then I passed the PageArray to the $map->render($MY_PAGE_ARRAY, 'MY_MAP_FIELD', $options); it works.

So I suggest to collect your points in a PageArray with proper selectors. Don't forget that your repeater has a template, and if useful you can get the parents of them.

 

Let me know if this solution can fit your needs.

Edited by Cybermano
Link to comment
Share on other sites

2 hours ago, torf said:

Thanks @Cybermano I already did it with page arrays and it works great, but this time I'd prefer a repeater. Using page arrays would make the backend to complicated.

Hi @torf, maybe I didn't get it at all or maybe I didn't explain well my idea.
So I'll try again... 😉

Surely I could be wrong, but for me a PageArray is not only for admin side, but all the pw pages collected into a php variable created with pw selectors in a script file.

For shortness I have created 2 pages with two repeaters each (repeater name is "multipoints"), with a title and a leaflet map field (named 'fm_mappa') into.
This is the simply code for testing. Please note that I didn't get the repeater with a foreach statement, but with a pw search with a selector for all the templates with name equals to "repeater_NAMEOFTHEREPEATER", as pw builds. Obviously into your selector you can build your search to the desired points, e.g with sorting or something other.

echo $pagesWithMultipointsRepeater = wire('pages')->find('template=repeater_multipoints');
        if (count($pagesWithMultipointsRepeater)){
            $map = $modules->get('MarkupLeafletMap');       
            $options = array(
                'height' => 800,
                'markerColour' => 'red',
            );
            echo '<div class="g-mb-25">';
            echo $map->render($pagesWithMultipointsRepeater,'fm_mappa',$options);
            echo '</div>';
        }

The numbers before the map are the repeater ids (note the "echo" on the first line of the code).

1796299206_Thefrontendmap.thumb.png.93c6289220481a0d9d893e5fc000cac1.png

And these are the admin pages:

298966150_Page1.thumb.png.84c204957b92a3efd15b1b0abe261288.png 918678329_Page2.thumb.png.3552138362ac88df8af0689c9ed318d9.png


I hope that's clearer.

  • Thanks 1
Link to comment
Share on other sites

Or maybe this edited code of your first post works?


$map = $modules->get('MarkupLeafletMap');
echo $map->getLeafletMapHeaderLines();
$mappoints = new PageArray();
foreach ($page->map_repeater as $point) {
	$mappoints->add($point) // the repater is a page
	}
echo $map->render($mappoints, 'your_map_field_name', array('height' => '270px'));
?>

 

Link to comment
Share on other sites

For the records: I found a way to use multiple points or areas directly from a repeater, but it's not the most beautiful method. It works by simply omitting the frontend part of the module.

I just load the leaflet js and css manually, extract the lat and lng from the repeater, and paste them in the site. Which has the advantage that I can use additional fields in my repeater (in my example I use a field named "radius" to draw multiple circles:

<div id="map" style="height: 200px"></div>
<script>
    var map = L.map('map').setView([51.505, -0.09], 13);
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
    let boundaries = [];
    <?php
    foreach($page->my_repeater as $repeaterdata) {
        $diameter = $repeaterdata->radius * 1000;
        echo "
        var circle = L.circle([{$repeaterdata->map->lat}, {$repeaterdata->map->lng}], {
        color: 'red',
        fillColor: '#f03',
        fillOpacity: 0.5,
        radius: {$diameter}
    }).addTo(map);
        ";
    }

    ?>
</script>

Please note that this is just a proof of concept. I haven't played around with markerclusters, automatic boundaries or any other fancy stuff at this point.

  • Like 2
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...