On my to do list for next year is to finally get round to doing something consistently with open data in an Isle of Wight context, probably on isleofdata.com. One of the things I particularly want to explore are customisable WordPress plugins that either source data from on online data source or that can be configured as part of an external publishing system.
For example, the following code, saved as MultiMarkerLeafletMap2.php and zipped up implements a WordPress plugin that can render an interactive leaflet map with clustered markers.
<?php /* Plugin Name: MultiMarkerLeafletMap2 Description: Shortcode to render an interactive map displaying clustered markers. Markers to be added as JSON. Intended primarily to supported automated post creation. Inspired by folium python library and Google Maps v3 Shortcode multiple Markers WordPress plugin Version: 1.0 Author: Tony Hirst */ function MultiMarkerLeafletMap2_custom_styles() { wp_deregister_style( 'oi_css_map_leaflet' ); wp_register_style( 'oi_css_map_leaflet', '//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.css',false, '0.7.3' ); wp_enqueue_style( 'oi_css_map_leaflet' ); wp_deregister_style( 'oi_css_map_bootstrap' ); wp_register_style( 'oi_css_map_bootstrap', '//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css', false, '3.2.0' ); wp_enqueue_style( 'oi_css_map_bootstrap' ); wp_deregister_style('oi_css_map_bootstrap_theme'); wp_register_style('oi_css_map_bootstrap_theme','//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css',false,false); wp_enqueue_style( 'oi_css_map_bootstrap_theme'); wp_deregister_style('oi_css_map_fa'); wp_register_style('oi_css_map_fa','//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css',false,false); wp_enqueue_style( 'oi_css_map_fa'); wp_deregister_style('oi_css_map_lam'); wp_register_style('oi_css_map_lam','//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css',false,false); wp_enqueue_style( 'oi_css_map_lam'); wp_deregister_style('oi_css_map_lmcd'); wp_register_style('oi_css_map_lmcd','//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css',false,false); wp_enqueue_style( 'oi_css_map_lmcd'); wp_deregister_style('oi_css_map_lmc'); wp_register_style('oi_css_map_lmc','//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.css',false,false); wp_enqueue_style( 'oi_css_map_lmc'); wp_deregister_style('oi_css_map_lar'); wp_register_style('oi_css_map_lar','//birdage.github.io/Leaflet.awesome-markers/dist/leaflet.awesome.rotate.css',false,false); wp_enqueue_style( 'oi_css_map_lar'); } function MultiMarkerLeafletMap2_custom_scripts() { wp_deregister_script( 'oi_script_leaflet' ); wp_register_script( 'oi_script_leaflet', '//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.js',array('oi_script_jquery'),'0.7.3'); wp_enqueue_script( 'oi_script_leaflet' ); wp_deregister_script( 'oi_script_jquery' ); wp_register_script( 'oi_script_jquery', '//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js', false, '1.11.3' ); wp_enqueue_script( 'oi_script_jquery' ); wp_deregister_script( 'oi_script_bootstrap' ); wp_register_script( 'oi_script_bootstrap', '//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js',false,'3.2.0'); wp_enqueue_script( 'oi_script_bootstrap' ); wp_deregister_script( 'oi_script_lam' ); wp_register_script( 'oi_script_lam', '//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js',array('oi_script_leaflet'),'2.0'); wp_enqueue_script( 'oi_script_lam' ); wp_deregister_script( 'oi_script_lmc' ); wp_register_script( 'oi_script_lmc', '//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster.js',array('oi_script_leaflet'),'0.4.0'); wp_enqueue_script( 'oi_script_lmc' ); wp_deregister_script( 'oi_script_lmcsrc' ); wp_register_script( 'oi_script_lmcsrc', '//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster-src.js',array('oi_script_leaflet'),'0.4.0'); wp_enqueue_script( 'oi_script_lmcsrc' ); } add_action( 'wp_enqueue_scripts', 'MultiMarkerLeafletMap2_custom_scripts' ); add_action( 'wp_enqueue_scripts', 'MultiMarkerLeafletMap2_custom_styles' ); // Add items to header add_action('wp_head', 'MultiMarkerLeafletMap2_header'); add_action('wp_head', 'MultiMarkerLeafletMap2_fix_css'); function MultiMarkerLeafletMap2_fix_css() { echo '<style type="text/css">#map { position:absolute; top:0; bottom:0; right:0; left:0; }</style>' . "\n"; } function MultiMarkerLeafletMap2_header() { } function MultiMarkerLeafletMap2_call($attr) { // Generate the map template // Default attributes - can be overwritten from shortcode $attr = shortcode_atts(array( 'lat' => '0', 'lon' => '0', 'id' => 'oimap_1', 'zoom' => '7', 'width' => '600', 'height' => '400', 'type' => 'multimarker', 'markers'=>'' ), $attr); $html = '<div class="folium-map" id="'.$attr['id'].'" style="width: '. $attr['width'] .'px; height: '. $attr['height'] .'px"></div> <script type="text/javascript"> var base_tile = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 18, minZoom: 1, attribution: "Map data (c) OpenStreetMap contributors - http://openstreetmap.org" }); var baseLayer = { "Base Layer": base_tile }; /* list of layers to be added */ var layer_list = { }; /* Bounding box. */ var southWest = L.latLng(-90, -180), northEast = L.latLng(90, 180), bounds = L.latLngBounds(southWest, northEast); /* Creates the map and adds the selected layers */ var map = L.map("'.$attr['id'].'", { center:['.$attr['lat'].', '.$attr['lon'].'], zoom: '.$attr['zoom'].', maxBounds: bounds, layers: [base_tile] }); L.control.layers(baseLayer, layer_list).addTo(map); //cluster group var clusteredmarkers = L.markerClusterGroup(); //section for adding clustered markers '; if($attr['type']=='multimarker'){ // Get our custom fields global $post; $premarkers=get_post_meta( $post->ID, 'markers', true ); $markers = json_decode($premarkers,true); $legend = get_post_meta( $post->ID, 'maplegendtemplate', true ); $legendkeys = get_post_meta( $post->ID, 'maplegendkeys', true ); if (count($markers)>0){ for ($i = 0;$i < count($markers);$i ++){ $popup=$legend; foreach (explode(',', $legendkeys) as $k) { $popup=str_replace("%".$k."%",$markers[$i][$k],$popup); }; $html .=' var marker_'.$i.'_icon = L.AwesomeMarkers.icon({ icon: "info-sign",markerColor: "blue",prefix: "glyphicon",extraClasses: "fa-rotate-0"}); var marker_'.$i.' = L.marker(['.$markers[$i]['lat'].','.$markers[$i]['lon'].'], {"icon":marker_'.$i.'_icon}); marker_'.$i.'.bindPopup("'.$popup.'"); marker_'.$i.'._popup.options.maxWidth = 300; clusteredmarkers.addLayer(marker_'.$i.'); '; } } } $html .= '//add the clustered markers to the group anyway map.addLayer(clusteredmarkers);</script>'; return $html; ?> <?php } add_shortcode('MultiMarkerLeafletMap2', 'MultiMarkerLeafletMap2_call'); ?>
Data is passed to the plugin embedded in a WordPress post via three custom fields associated with the post:
- markers: a JSON list that contains information associated with each marker;
- maplegendkeys: a comma separated list of key values that refers to keys in each marker object that are referenced when constructing the popup legend for each marker;
- maplegendtemplate: a template that is used to construct each popup legend, of the form ‘Asset type: %typ% (%loc%)’, where the %VAR% elements identify key vales VAR associated with object attributes in the markers list.
In the set up I have, the post content – including the plugin code – is generated from a Python script running in a Jupyter notebook that can be posted using the following code fragment:
#!pip3 install python-wordpress-xmlrpc from wordpress_xmlrpc import Client, WordPressPost from wordpress_xmlrpc.compat import xmlrpc_client from wordpress_xmlrpc.methods import media, posts from wordpress_xmlrpc.methods.posts import NewPost wpoi = Client(WORDPRESS_BLOG_URL+'/xmlrpc.php', 'robot1', WORDPRESS_API_KEY) def wp_customPost(client,title='ping',content='pong, <em>pong<em>',custom={}): post = WordPressPost() post.title = title post.content = content post.custom_fields = [] for c in custom: post.custom_fields.append({ 'key': c, 'value': custom[c] }) response = client.call(NewPost(post)) return response
A list of objects is created from a pandas dataframe where each object contains the information associated with each marker – we limit the list to only include items for which we have latitude and longitude information:
def itemiser(row): item={'lat':row['latlong'].split(',')[0], 'lon': row['latlong'].split(',')[1], 'typ':row['Asset Type Description'], 'tenure':row['Tenure'], 'loc':row['Location'], 'location':'{}, {}, {}'.format(row['Address 1'], row['Address 2'], row['Post Code']), } return item jd1=df[(df['latlong']!='') & (df['latlong'].notnull())].apply(itemiser,axis=1)
A post is then constructed that includes a reference to the plugin (as part of the text of the body of the post) and the data that is to be passed to the custom variables.
import json #txt contains the content for the blog post txt="[MultiMarkerLeafletMap2 zoom=11 lat=50.675 lon=-1.31 width=800 height=500]" txt='{}{}'.format(txt,' <div><em>Data produced by <a href="https://www.iwight.com/Council/transparency/Our-Assets/Transparency-Our-Assets/Property">Isle of WIght Council</a>.</div> ') #jsondata contains the custom variable data that will be associated with the post jsondata={'markers':json.dumps( jd1.tolist() ), 'maplegendkeys':'typ,tenure,location,loc', 'maplegendtemplate':'Asset type: %typ% (%loc%)<br/>Tenure: %tenure%<br/>%location%'} wp_customPost(wpoi, "Properties on the Isle of Wight Council property register", txt, jsondata)