function maps_init() {

    console.log( 'maps init' );

    if ( typeof $ === 'undefined' ) $ = jQuery;

    /**
     * Load the module.
     */
    $(function() {
        // init check
        if ($('.gMaps').length == 0) {
            console.warn('no maps found on this page');
            console.warn('maps script aborted');
            return false;
        }
        if ($('.gMaps').length > 1) {
            console.warn('multiple maps are found on this page');
            console.warn('only the first map will be rendered');
        }
        // only init the first map on the page
        addEvents( $('.gMaps')[0] );
        console.log("maps script initialized");
    });

    /**
     * Holds the class objects
     */
    var map = null;
    var container = null;
    var icons = { pin: "", cluster: "" };
    var locations = [];
    var pins = [];
    var cluster = null;

    /**
     * Add the events.
     */
    const addEvents = () => {
        if ( typeof google === 'undefined' ) {
            console.warn( 'Global object "google" is not defined.' );
            return false;
        }

        console.log( "maps script loaded" );

        container = $('.gMaps')[0];
        map = new google.maps.Map( container, {
            zoom: 4,
            center: new google.maps.LatLng(50, 10),
            // disableDefaultUI: true,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });

        map.infoBox = new InfoBox({
            content: 'Loading...',
            disableAutoPan: false,
            pixelOffset: new google.maps.Size(20, -90),
            infoBoxClearance: new google.maps.Size(1, 1)
        });

        google.maps.event.addListener(map, 'zoom_changed', function() {
            map.infoBox.close();
        });
    
        $('div[id^="filter_"] select')[0].addEventListener("change", function (e) {
            console.log( 'changed', changeFilter );
            changeFilter( $('div[id^="filter_"] select').val() )
        })
        
        // get icons
        icons.pin = $( container ).data('pin');
        icons.cluster = $( container ).data('cluster');
    
        // get locations and add markers
        readLocations();
    }

    /**
     * Functions
     */
    const clearLocations = () => {
        console.log("clear locations");
        clearMarkers();
        locations = [];
    }

    const readLocations = () => {
        console.log("read locations");
        clearMarkers();
        locations = JSON.parse(decodeURIComponent( $( container ).data('locations')) );
        addMarkers();
    }

    const clearMarkers = () => {
        map.infoBox.close();
        $.each(pins, function(i, pin) {
            if (pin != undefined) {
                pins[i].setMap(null);
            }
        });
        pins = [];
        if (cluster != null) {
            cluster.clearMarkers();
            cluster = null;
        }
    }

    const addMarkers = () => {
        if (locations.length > 0) {
            console.log("add marker");
            setMarkers( $( container ).data('filter') );
        }
        else console.warn("no locations");
    }

    const setMarkers = (filter) => {
        if ( pins.length > 0 || cluster != null ) {
            clearMarkers();
        }
        var bounds = new google.maps.LatLngBounds();
        
        $.each(locations, function( i, loc ) {
            if ( typeof loc === 'undefined' ) return;

            // filter by tax term
            if ( filter == null || filter == -1 || (loc["filter"].indexOf(filter) != -1) ) {

                // make marker pin
                var pos = new google.maps.LatLng(loc["lat"], loc["lon"]);
                if (!isNaN(pos.lat()) && !isNaN(pos.lng())) {
                    var marker = new google.maps.Marker({
                        map: map,
                        position: pos,
                        icon: icons.pin,
                        title: loc["title"],
                        html: loc["content"]
                    });
                    pins[loc["ID"]] = marker;
                    bounds.extend(marker.position);
                    // marker events
                    google.maps.event.addListener(marker, 'click', function () {
                        map.infoBox.setContent(this.html);
                        map.infoBox.open(map, this);
                    });
                }

                // show post result
                $( '.wp-block-query .search_result[data-post="'+ loc.ID +'"]' ).removeClass('hidden');
            }
            else {

                // hide post result
                $( '.wp-block-query .search_result[data-post="'+ loc.ID +'"]' ).addClass('hidden');
            }

        });

        // console.log(pins);
        if (pins.length > 0) {
            cluster = new MarkerClusterer(map, pins.filter(n => n), { gridSize: 30, maxZoom: 15, imagePath: icons.cluster });
            map.fitBounds(bounds);
        }
        // else console.log("no locations");
    }

    const openMarker = (id) => {
        if (typeof id === 'object') {
            id = $(id).closest('.search_result').data('post');
        }
        map.infoBox.close();
        if (pins[id] != undefined) {
            console.log("open marker: "+id);
            map.infoBox.setContent(pins[id].html);
            map.panTo(pins[id].position);
            google.maps.event.addListenerOnce(map, 'idle', function() {
                // console.log('arrived at location');
                map.infoBox.open(map, pins[id]);
            });
            nav.scrollTo( $(container).offset().top );
        }
        else console.warn("unknown marker: "+id);
    };

    const changeFilter = (val) => {
        console.log( "change filter: " + val );
        setMarkers( parseInt( val ) );
    }

}
