Custom Fields ● OpenStreetMap

with plenty of features

by Marc Dechèvre

Marc

present slides https://slides.woluweb.be

Basic Joomla Tutorials 2020.01.07 | @basicjoomla YouTube

1. Goal of this presentation

The goal is to assign an OpenStreetMap Custom Field to one or several categories. So each article (being Members, Events, whatever) will be geolocalized.

And based on this we will create a global OpenStreetMap showing all the locations.We will start with a simple multi-markers map.

And then we will even end up with:

2. What we will achieve

cf-osm

3. The online demo

https://joomlacustomfields.org/en/

4. OpenStreetMap – Leaflet

In order to create our OpenStreetMap, we will use the following open-source Javascript library: https://leafletjs.com/

See the following examples: https://leafletjs.com/examples.html

Note : don’t forget of course to add the link to the .js file and to the .css file

5. OSM Custom Fields

I have found 3 OpenStreetMap Custom Fields out there.

Each has its strengths :)

5.1. Advanced Custom Fields by Tassos Marinos

Free and Paid (€ 19)

Includes 25+ Custom Fields, among which OSM, Google Maps & Bing Maps

https://www.tassos.gr/joomla-extensions/advanced-custom-fields https://www.tassos.gr/joomla-extensions/advanced-custom-fields/docs/the-openstreetmap-field

5.2. OpenStreetMap Custom Field by Nordmograph

Paid (€ 10)

There is also a Google Maps field. There are also extensions allowing to build maps linked to different sources (Articles but also third-party extensions. You can really build crazy things (see example during this live session)

https://extensions.joomla.org/extensions/extension/authoring-a-content/custom-fields/openstreetmap-custom-field/

5.3. OpenStreetMap by GMapFP

Free

There are also plenty of maps-related extensions by GMapFP.

https://creation-web.pro/extensions-joomla-francaises/46-field-osm

6. The chosen OSM Custom Field

For the sake of this presentation we will use the OSM Custom Field, being part of Advanced Custom Fields by Tassos Marinos.

The reason being that it will only write the Latitude/Longitude in the database (without extra informations like a personalized marker, a text, a zoom level or whatever).

So it will be easier for us to manipulate this Custom Field to create our multi-markers map.

7. Your own map-marker

Instead of PNG image, I chose SVG image for two reasons :

  1. vector => image is always perfect whatever its size
  2. easy to edit to change colors : you don’t need Photoshop… just edit it with your basic Text Editor

The present SVG was found on https://www.iconfinder.com/search/?q=map+marker&from=navbar

8. Preparing the Articles

In the present demo, we will create two Categories

Then we install the OSM Custom Field.

Finally, we create a OSM Custom Field and assign it to the 2 above categories.

9. OSM without override

How could we possibibly loop through articles of different categories without playing with Overrides / Alternate Layouts ?

Actually, simply by using Articles Anywhere by RegularLabs

https://www.regularlabs.com/extensions/articlesanywhere

The only drawback compared to making overrides : we will need the Pro version (the Free version does not allow to loop through articles).In many cases it would be enough, but in order to avoid end-users to “break” the code we would put in an Article or in a Module (and in this case also because our Editor would strip some part of our code), we will also use ReReplacer by RegularLabs

https://www.regularlabs.com/extensions/rereplacer

This allows us to replace a shortcode that we create like {osm} by our code (and we can even use regular expressions which can be handy).

10. Articles Anywhere – right configuration

Have the following order in the Plugins:

  1. System – Regular Labs – ReReplacer
  2. System – Regular Labs – Articles Anywhere

To avoid having the Comments START: Articles Anywhere in the HTML before/after the content -which would prevent the display of the map-markers- go to plugin Articles Anywhere > Advanced > set “Place HTML comments” to NO.

11. Articles Anywhere – syntax

Let’s assume we have a Category called JoomlaDays and that we created an OSM Custom Field with Name “open-street-name”

The following code

{articles category="JoomlaDays"}
[open-street-map output="value"]
{/articles}

will output

50.84717044999999,4.35198095255952
50.842995099999996,4.43528195676678

Interesting feature : the possibility to use syntax like category="current" https://www.regularlabs.com/extensions/articlesanywhere/tutorial#multiple-articles-filters-dynamic-values

11.1. OSM without override 1 – screenshot

just one Category | custom Marker (svg) | Tooltip with link to Article

osm1

11.2. OSM without override 1 – code

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>
<div id="map" style="width: 100%; height: 400px;">&nbsp;</div>
<script>
    var map = L.map('map').setView([50.84717044999999,4.35198095255952], 12);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

       var MyOwnIcon = L.icon({
           iconUrl: '/images/map-marker.svg',
           iconSize: [38, 38],
       });

{articles category="Recipes"}
    L.marker([[open-street-map output="value"]], {icon: MyOwnIcon}).bindPopup('[link][title][/link]').addTo(map);
{/articles}

</script>

11.3. OSM without override 1 – comments

Note :

"[link][title][/link]"

would not work because of double quotes in the Link… which are not “escaped”

2 solutions :

'[link][title][/link]'
"<a href=\"[sefurl]\">[title]</a>"

11.4. OSM without override 2 – screenshot

several Categories – different Marker for each Category

osm2

11.5. OSM without override 2 – code

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>
<div id="map" style="width: 100%; height: 400px;">&nbsp;</div>
<script>
    var map = L.map('map').setView([50.84717044999999,4.35198095255952], 6);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    var MyOwnIcon = L.Icon.extend({
        options: {
            iconSize: [40, 40],
            iconAnchor: [20, 40],
            popupAnchor:  [0, -45]
        }
    });

    var jugsIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-blue.svg'
        }),
        joomladaysIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-green.svg'
        });

{articles category="joomla-events" include_child_categories="true"}
    L.marker([[open-street-map output="value"]], {icon: [category-alias]Icon}).bindPopup('[link][title][/link]').addTo(map);
{/articles}

</script>

11.6. OSM without override 3 – screenshot

tooltip opens automatically

osm3

11.7. OSM without override 3 – code

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>
<div id="map" style="width: 100%; height: 400px;">&nbsp;</div>
<script>
    var map = L.map('map').setView([50.84717044999999,4.35198095255952], 6);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    var MyOwnIcon = L.Icon.extend({
        options: {
            iconSize: [40, 40],
            iconAnchor: [20, 40],
            popupAnchor:  [0, -45]
        }
    });

    var jugsIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-blue.svg'
        }),
        joomladaysIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-green.svg'
        });

L.control.scale().addTo(map);

{articles category="joomla-events" include_child_categories="true" order="title"}
    L.marker([[open-street-map output="value"]], {icon: [category-alias]Icon}).bindPopup('[link][title][/link]').addTo(map).openPopup();
{/articles}

</script>

{articles category="joomla-events" include_child_categories="true"}
    [link][title][/link]<br />
{/articles}

11.8. OSM without override 3 – comment

.addTo(map).openPopup()

The openPopup() opens then the popup (so for the last article of the loop since only one is shown at the time)

12. OSM with override

12.1. Modules: Articles – Newsflash

As you remember from the previous presentations, in an override / alternate layout we can display the Custom Field having ID “X” with the following line of code :

<?php echo $this->item->jcfields[X]->value; ?>

This works automatically for many Views, but not for all of them.

For example, in Modules: Articles – Newsflash, if you set “Trigger Plugin Events” to YES, then the variable jcfields will be available. But if you set it to NO, the Custom Fields won’t be readily available.

And for this presentation I don’t want to use Modules: Articles – Newsflash because it has too many options (so one might get confused by the lenght of the override).

12.2. Modules: Articles – Newsflash

Therefore, I prefer to use Modules: Articles – Latest because it has just the Options we need :

The only drawback is that jcfields is not readily available. Nevermind, Alexandre ELISE shared a few line of codes allowing to use Custom Fields by ID and even by Name in such a case: https://gist.github.com/alexandreelise/e04d417c9f911ce2ab2a3e931142e89b

12.3. Override print_r

if you want to see what is available for each Article

<?php
/**
 * @package     Joomla.Site
 * @subpackage  mod_articles_latest
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;
?>

<?php 
foreach ($list as $item) {
    echo '<pre>' . print_r($item, true) . '</pre>';
}
?>

12.4. OSM with override – basic – screenshot

multiple Categories | tooltip with link to Article | standard Marker

osmbasic

12.5. OSM with override – basic – code

<?php
/**
 * @package     Joomla.Site
 * @subpackage  mod_articles_latest
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;
?>

<h2>Override for OpenStreetMap</h2>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>
<div id="map" style="width: 100%; height: 400px;">&nbsp;</div>
<script>
    var map = L.map('map').setView([50.84717044999999,4.35198095255952], 6);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    <?php foreach ($list as $item) : ?>

      <?php
          // example made after an exchange between Marc Dechèvre and Alexandre Elisé
          // https://gist.github.com/alexandreelise/e04d417c9f911ce2ab2a3e931142e89b
          $jcfields = FieldsHelper::getFields('com_content.article', $item, true);
          // custom fields by name
          // usage: $fields_by_name['name-of-field']->value
          $fields_by_name = \Joomla\Utilities\ArrayHelper::pivot($jcfields, 'name');
      ?>

       <?php if (!empty($fields_by_name['open-street-map']->rawvalue)): // add a marker only if lat/long is not empty otherwise would get error ?>
        <?php // use htmlspecialchars($item->title ,ENT_QUOTES) for the Title and not just $item->title otherwise the map would not display if the Title would for example contain a simple quote ?>
      L.marker([<?php echo $fields_by_name['open-street-map']->rawvalue; ?>]).bindPopup('<a href="<?php echo $item->link; ?>"><?php echo htmlspecialchars($item->title, ENT_QUOTES); ?></a>').addTo(map);
    <?php endif ?>

    <?php endforeach; ?>

</script>

12.6. OSM with override – intermediate – screenshot

diff. Marker for each Category | tooltip with extra Custom Fields

osmintermediate

12.7. OSM with override – intermediate – code

<?php
/**
 * @package     Joomla.Site
 * @subpackage  mod_articles_latest
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;
?>

<h2>Override for OpenStreetMap</h2>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>

<div id="map" style="width: 100%; height: 400px;"></div>
<script>
    var map = L.map('map').setView([50.84717044999999,4.35198095255952], 6);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    var MyOwnIcon = L.Icon.extend({
        options: {
            iconSize: [40, 40],
            iconAnchor: [20, 40],
            popupAnchor:  [0, -45]
        }
    });

    var jugsIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-blue.svg'
        }),
        joomladaysIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-green.svg'
        }),
        enIcon = new MyOwnIcon({
            iconUrl: '/images/map-marker-red.svg'
        });

    L.control.scale().addTo(map);

    <?php foreach ($list as $item) : ?>

      <?php
          // example made after an exchange between Marc Dechèvre and Alexandre Elisé
          // https://gist.github.com/alexandreelise/e04d417c9f911ce2ab2a3e931142e89b
          $jcfields = FieldsHelper::getFields('com_content.article', $item, true);
          // custom fields by name
          // usage: $fields_by_name['name-of-field']->value
          $fields_by_name = \Joomla\Utilities\ArrayHelper::pivot($jcfields, 'name');
      ?>

       <?php if (!empty($fields_by_name['open-street-map']->rawvalue)): // add a marker only if lat/long is not empty otherwise would get error ?>
        <?php // use htmlspecialchars($item->title ,ENT_QUOTES) for the Title and not just $item->title otherwise the map would not display if the Title would for example contain a simple quote ?>

    var customPopup = '<a href="<?php echo $item->link; ?>"><?php echo htmlspecialchars($item->title, ENT_QUOTES); ?></a><br /><?php echo $fields_by_name['country']->value; ?>';

      L.marker([<?php echo $fields_by_name['open-street-map']->rawvalue; ?>], {icon: <?php echo $item->category_alias; ?>Icon}).bindPopup(customPopup).addTo(map);
    <?php endif ?>

    <?php endforeach; ?>

</script>

12.8. OSM with override – advanced – screenshot

Clustering | Layers with Filters

osmadvanced

12.9. OSM with override – advanced – code

<?php
/**
 * @package     Joomla.Site
 * @subpackage  mod_articles_latest
 *
 * @copyright   Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;
?>

<?php
use Joomla\CMS\Uri\Uri;

$document = JFactory::getDocument();

// A function used when we need to pass HTML code into a Javascript variable (article's title and introduction) 
function convertHtmlToJsString($string)
{
    $string = str_replace("\r", "", $string);
    $string = str_replace("\n", "", $string);
    return addslashes($string);
}
?>

<h2>Override for OpenStreetMap</h2>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>

<link rel="stylesheet" href="https://leaflet.github.io/Leaflet.markercluster/dist/MarkerCluster.css" crossorigin=""/>
<link rel="stylesheet" href="https://leaflet.github.io/Leaflet.markercluster/dist/MarkerCluster.Default.css" crossorigin=""/>
<script src="https://leaflet.github.io/Leaflet.markercluster/dist/leaflet.markercluster-src.js" crossorigin=""></script>

<div id="map" style="width: 100%; height: 400px;"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 
    var icon_dirname = '<?php echo Uri::root();?>/images';
    var map_center_latlon = [50.84717044999999,4.35198095255952];
    var map_zoom_initial = 6;
    var use_clustering = true; // true | false

    // Declare icons
    var MyOwnIcon = L.Icon.extend({
        options: {
            iconSize: [40, 40],
            iconAnchor: [20, 40],
            popupAnchor:  [0, -45]
        }
    });
    var jugsIcon = new MyOwnIcon({
        iconUrl: icon_dirname + '/map-marker-blue.svg'
    });
    var joomladaysIcon = new MyOwnIcon({
        iconUrl: icon_dirname + '/map-marker-green.svg'
    });
    //
    var map = L.map('map').setView(map_center_latlon, map_zoom_initial);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
    L.control.scale().addTo(map);

    // Because we use "overlays" (checkbox markers selection) we need layergroups.
    // See : https://leafletjs.com/reference-1.6.0.html#control-layers
    var layerGroups = {};
    var layergroup_name;
<?php foreach ($list as $item) : ?>
<?php
    // example made after an exchange between Marc Dechèvre and Alexandre Elisé
    // https://gist.github.com/alexandreelise/e04d417c9f911ce2ab2a3e931142e89b
    $jcfields = FieldsHelper::getFields('com_content.article', $item, true);
    // custom fields by name
    // usage: $fields_by_name['name-of-field']->value
    $fields_by_name = \Joomla\Utilities\ArrayHelper::pivot($jcfields, 'name');

    // Check if custom field "open-street-map" is filled (should add some more checks)
    if(isset($fields_by_name['open-street-map']) && trim($fields_by_name['open-street-map']->rawvalue) != ''){
?>
<?php $article_images  = json_decode($item->images); ?> 

    layergroup_name = '<?php echo $item->category_alias;?>';
    // Create a layergroup for the article's category (only once by category).
    if(layerGroups[layergroup_name]  undefined){
        if(use_clustering  true){
            layerGroups[layergroup_name] = L.markerClusterGroup();
        }else{
            layerGroups[layergroup_name] = L.layerGroup(); 
        }
    }
    // Add the marker into the layergroup
    var marker = L.marker([<?php echo $fields_by_name['open-street-map']->rawvalue; ?>], {icon: <?php echo $item->category_alias; ?>Icon});
    // Use htmlspecialchars($item->title ,ENT_QUOTES) for the Title and not just $item->title otherwise the map would not display if the Title would for example contain a simple quote.
    var title = <?php echo "'" . convertHtmlToJsString($item->title) . "'" ?>;
    var introduction = <?php echo "'" . convertHtmlToJsString($item->introtext) . "'" ?>;
    var introimage='';
    <?php if(!empty($article_images->image_intro))  echo "var introimage = '<img src=\"" . $article_images->image_intro ."\"style=\"width: 200px;\">'"  ?>;

    marker.bindPopup('<a href="<?php echo $item->link; ?>">' + title + '</a>'+ introduction + introimage);
    layerGroups[layergroup_name].addLayer(marker);
<?php
    }else{
        // If custom field is not filled, add some debug comments
        echo "// " . $fields_by_name['open-street-map']->rawvalue . "\n";
        echo "// " . $item->title . "\n";
    }
?>
<?php endforeach; ?>
    // Add layergroups to the map

    for(var layergroup_name in layerGroups){
        layerGroups[layergroup_name].addTo(map);
    }
    // Add "overlays" (checkboxes for each category) to the map
    var baseLayers = {};
    var overlays = layerGroups;
    L.control.layers(baseLayers, overlays).addTo(map);
});
</script>

13. Thank you

For giving me the idea of this presentation and his example

Philippe COMBET http://www.adosis.com

For the trick to easily integrate Custom Fields in Overrides where it is not foreseen

Alexandre ELISE https://alexandre-elise.fr

For the Clustering and the Layers

Philippe LAMBOTTE https://www.m4ucode.be

For their Custom Fields and the new versions following our exchanges

Tassos MARINOS https://www.tassos.gr

Adrien ROUSSEL https://www.nordmograph.com

Fabrice PELLETIER https://www.gmapfp.org

For organizing all those streaming sessions Tim DAVIS https://www.basicjoomla.com/

For being #jPositive

So many members of the Joomla Community ;)

14. Get in touch

https://slides.woluweb.be

Any suggestion about this presentation ?
Please feel free to contact me. I’ll be happy to keep improving it:)

Marc Dechèvre | woluweb
+32 474 37 13 12 | +32 2 772 58 69

https://www.woluweb.be/contact

https://www.slideshare.net/woluweb