0

I'm trying to put together a page that has several maps (one that contains all of a group of routes and one each for each route), each in a different (Bootstrap) tab. I got it to work with the most repetitive code, by repeating and independently assigning to each map the click and pointermove events I would like, but ideally I would assign these in some kind of loop.

My attempt is what I've pasted below, which works, but only for the final map - if I change the order of the maps any of them will work, but only so long as it is the last. I wasn't able to find much direction on what I'm looking for, this other question being the closest, which mentions the same issue of only working on the last map, though I wasn't able to make much use of the answer provided. Is there a way to loop through these events and have them work over multiple views/maps?

Any direction is greatly appreciated!

Javascript:

//----------/MAP 0/----------//

var myView0 = new ol.View({
    center: ol.proj.fromLonLat([-87.068669, 8.470072]),
    zoom: 3,
});

var map0 = new ol.Map({
    layers: [
        new ol.layer.Tile({
            source: new ol.source.OSM()
        }),
        vectorLayerJNY10,
        vectorLayerJNY11,
        vectorLayerJNY12,
        hoverLayers['route'],
    ],
    target: 'map0',
    view: myView0,
});

//----------/MAP 1/----------//

var myView1 = new ol.View({
    center: ol.proj.fromLonLat([-94.803044, 36.719405]),
    zoom: 3.5,
});

var map1 = new ol.Map({
    layers: [
        new ol.layer.Tile({
            source: new ol.source.OSM()
        }),
        vectorLayerJNY10,
        hoverLayers['route'],
    ],
    target: 'map1',
    view: myView1,
});

//----------/MAP 2/----------//

var myView2 = new ol.View({
    center: ol.proj.fromLonLat([-114.778097, 32.698996]),
    zoom: 4,
});

var map2 = new ol.Map({
    layers: [
        new ol.layer.Tile({
            source: new ol.source.OSM()
        }),
        vectorLayerJNY11,
        hoverLayers['route'],
    ],
    target: 'map2',
    view: myView2
});

//----------/MAP 3/----------//

var myView3 = new ol.View({
    center: ol.proj.fromLonLat([-93.988196, 19.253431]),
    zoom: 4 
});

var map3 = new ol.Map({
    layers: [
        new ol.layer.Tile({
            source: new ol.source.OSM()
        }),
        vectorLayerJNY12,
        hoverLayers['route'],
    ],
    target: 'map3',
    view: myView3
});

var maps = [map0, map1, map2, map3];
var views = [myView0, myView1, myView2, myView3]

var mapOnClick = function (event) {
    if (event.map.hasFeatureAtPixel(event.pixel) === true) {
        var name = event.map.forEachFeatureAtPixel(event.pixel, function(feature) {
            return feature.get('name');
        })
        var coordinate = event.coordinate;
        content.innerHTML = name;
        olay.setPosition(coordinate);
    } else {
        olay.setPosition(undefined);
        closer.blur();
    }
};

var elmHover = function(evt) {
    if (evt.dragging) {
        return;
    }
    var pixel = evt.map.getEventPixel(evt.originalEvent);
    var map = elem;
    hover(pixel, map);
};

var cursor = function(evt) {
    elem.getTargetElement().style.cursor = elem.hasFeatureAtPixel(evt.pixel) ? 'pointer' : '';
};

var mapResize = function(event){
    setTimeout(function() {
        elem.updateSize();
    }, 200);
};

for(var i = 0; i < maps.length; i++){
    var elem = maps[i];

    var container = document.getElementById('popup' + i);
    var content = document.getElementById('popup-content' + i);
    var closer = document.getElementById('popup-closer' + i);

    var overlays = [];
    overlays[i] = new ol.Overlay({
        element: container,
        autoPan: true,
        autoPanAnimation: {
            duration: 250
        }
    });
    elem.addOverlay(overlays[i]);

    olay = overlays[i];
    view = views[i];

    elem.on('pointermove', elmHover);

    elem.on('singleclick', mapOnClick);

    elem.on('pointermove', cursor);

    closer.onclick = function() {
        layover.setPosition(undefined);
        closer.blur();
        return false;
    };

    $('.nav-tabs').on('shown.bs.tab', mapResize)

    $('.nav-tabs').on('hide.bs.tab', function(event){
        elem.setView(view);
        olay.setPosition(undefined);
    });
}

HTML (just in case :3):

<ul class="nav nav-tabs nav-justified" role="tablist">
    <li class="nav-item" >
        <a class="nav-link active" data-toggle="tab" href="#About">About</a>
    </li>
    <li class="nav-item" >
        <a class="nav-link" data-toggle="tab" href="#Journey-I">Journeys I</a>
    </li>
    <li class="nav-item" >
        <a class="nav-link" data-toggle="tab" href="#Journey-II">Journey II</a>
    </li>
    <li class="nav-item" >
        <a class="nav-link" data-toggle="tab" href="#Journey-III">Journey III</a>
    </li>
</ul>

<div class="tab-content">
    <div id="About" class="tab-pane fade show active" role="tabpanel">
        <h3>About</h3>

        <div id="map0" class="map"></div>

        <div id="popup0" class="ol-popup">
            <a href="#" id="popup-closer0" class="ol-popup-closer"></a>
            <div id="popup-content0"></div>
        </div>

    </div>

    <div id="Journey-I" class="tab-pane fade" role="tabpanel">
        <h3>Journey I</h3>

        <div id="map1" class="map"></div>

        <div id="popup1" class="ol-popup">
            <a href="#" id="popup-closer1" class="ol-popup-closer"></a>
            <div id="popup-content1"></div>
        </div>

    </div>

    <div id="Journey-II" class="tab-pane fade" role="tabpanel">
        <h3>Journey II</h3>

        <div id="map2" class="map"></div>

        <div id="popup2" class="ol-popup">
            <a href="#" id="popup-closer2" class="ol-popup-closer"></a>
            <div id="popup-content2"></div>
        </div>

    </div>

    <div id="Journey-III" class="tab-pane fade" role="tabpanel">
        <h3>Journey III</h3>

        <div id="map3" class="map"></div>

        <div id="popup3" class="ol-popup">
            <a href="#" id="popup-closer3" class="ol-popup-closer"></a>
            <div id="popup-content3"></div>
        </div>

    </div>

    <script src="{{ url_for('static', filename='JS/OLmap_ajall.js') }}" type="text/javascript"></script>

</div>

1 Answers1

2

Like was said in the question you check, it is a problem of context.

In the handlers of every event, you are using variables that take value on the loop, elem, olay, view. The problem is that when the events occur the value of these variables reference the last map, overlay, and view of their respective array.

There are several ways to solve this. I will propose you to use bind method of function (JavaScript - function bind).

var mapOnClick = function (event) {
    if (this.map.hasFeatureAtPixel(event.pixel) === true) {
        var name = this.map.forEachFeatureAtPixel(event.pixel, function(feature) {
            return feature.get('name');
        })
        var coordinate = event.coordinate;
        this.content.innerHTML = name;
        this.overlay.setPosition(coordinate);
    } else {
        this.overlay.setPosition(undefined);
        this.closer.blur();
    }
};

var elmHover = function(evt) {
    if (evt.dragging) {
        return;
    }
    var pixel = this.map.getEventPixel(evt.originalEvent);
    // var map = elem; <- what is this for?
    hover(pixel, this.map); // <- this function is not defined, look out
};

var cursor = function(evt) {
    this.map.getTargetElement().style.cursor = 
        this.map.hasFeatureAtPixel(evt.pixel) ? 'pointer' : '';
};

var mapResize = function(event){
    var self = this;
    setTimeout(function() {
        self.map.updateSize();
    }, 200);
};

var closerOnClick = function() {
    this.overlay.setPosition(undefined); // I am guessing is overlay
    this.closer.blur();
    return false;
}

var onHide = function(event){
    this.map.setView(this.view); // <- don't understand this
    this.overlay.setPosition(undefined);
}

var contexts = [
    { map: map0, view: myView0 },
    { map: map1, view: myView1 },
    { map: map2, view: myView2 },
    { map: map3, view: myView3 }
];

for(var i = 0; i < contexts.length; i++){
    contexts[i].container = document.getElementById('popup' + i);
    contexts[i].content = document.getElementById('popup-content' + i);
    contexts[i].closer = document.getElementById('popup-closer' + i);
    contexts[i].overlay = new ol.Overlay({
        element: contexts[i].container,
        autoPan: true,
        autoPanAnimation: {
            duration: 250
        }
    });
    contexts[i].map.addOverlay(contexts[i].overlay);
    contexts[i].mapOnClick = mapOnClick.bind(contexts[i]);
    contexts[i].elmHover = elmHover.bind(contexts[i]);
    contexts[i].cursor = cursor.bind(contexts[i]);
    contexts[i].mapResize = mapResize.bind(contexts[i]);

    contexts[i].map.on('singleclick', contexts[i].mapOnClick);
    contexts[i].map.on('pointermove', contexts[i].cursor);
    contexts[i].closer.onclick = contexts[i].closerOnClick;

    // don't understand why these are in the loop
    $('.nav-tabs').on('shown.bs.tab', contexts[i].mapResize);
    $('.nav-tabs').on('hide.bs.tab', contexts[i].onHide);
}

Note: There were several things that I did not understand of your code, plus you were initializing the overlays array in each iteration, and you have wrong variables names. It was long code, did not check it, so be aware there might be some issues. Is just for you to make the idea.

I noted that you are using jQuery, you could also use proxy to keep context. Although I notice that it is deprecated.

cabesuon
  • 4,860
  • 2
  • 15
  • 24
  • Thank you so very much for your very helpful answer, which largely resolves the issue. My apologies that I included only what I felt was the necessary portion of my code, and so for instance the 'hover' function is here only invoked but I did not feel was relevant to include itself (and works fine, once it is included where you left it out). I realize you didn't nor could I expect you to have thoroughly reviewed the code, but when you mention "wrong variable names" there was one that you did notice? – movethathoof Jun 07 '20 at 17:40
  • Also, the .nav-tabs functions are included as for the maps to appear on tabs activated after the first they must be resized (or so my experience [coincided with these](https://stackoverflow.com/questions/26034778/drawing-an-openlayers-3-map-in-a-hidden-element/26059779)) and the other because I want the map recentred and popups removed each time a tab is activated, and so the functions require the tab's map. That's actually the one part that isn't working, telling me that it "Cannot read property 'updateSize' of undefined". This.map works everywhere else, can you see a reason it doesn't there? – movethathoof Jun 07 '20 at 17:48
  • You are correct, the reason that it doesn't work is because in the context of the handler function of `setTimeout` `map` is not a property of `this`. You could do like the other cases, define a function and bind it to the correct context, or in this case you can do a little trick like assign `this` (or the reference you want) to a local variable. Check the updated code. – cabesuon Jun 07 '20 at 21:13
  • Again, many thanks for the very helpful answer! Now, though, the ```setView``` is not working, strangely - the ```setPosition``` for the overlay in the same ```onHide``` function does, and it does work if I assign it a view to give. Could this be like the ```setTimeout```, that ```view``` isn't a property of this? I tried separating it out into its own function, and your ```this``` assigning trick, but couldn't get it going. – movethathoof Jun 08 '20 at 02:49