<!DOCTYPE html>
<html>

  <head>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/ol3/3.8.2/ol.min.css">
    <link rel="stylesheet" href="style.css">
    
  </head>

  <body>
    <div id="map" tabindex="0"></div>
    
    <script src="//cdnjs.cloudflare.com/ajax/libs/ol3/3.8.2/ol.min.js"></script>
    <script src="ol-popup.js"></script>
    <script src="script.js"></script>
  </body>

</html>
function generatePointsCircle(count, centerPixel) {
  var 
      separation = 25,
      twoPi = Math.PI * 2,
      start_angle = twoPi / 12,
      circumference = separation * (2 + count),
      legLength = circumference / twoPi,  //radius from circumference
      angleStep = twoPi / count,
      res = [],
      i, angle;
  res.length = count;

  for (i = count - 1; i >= 0; i--) {
    angle = start_angle + i * angleStep;
    res[i] = [
      centerPixel[0] + legLength * Math.cos(angle), 
      centerPixel[1] + legLength * Math.sin(angle)
    ];
  }
  return res;
}
var olview = new ol.View({
    center: [-8347855.57, -679521.35],
    zoom: 10,
    minZoom: 2,
    maxZoom: 20
});

var sourceFeatures = new ol.source.Vector(),
    layerFeatures = new ol.layer.Vector({
        source: sourceFeatures
    });

var multiLineString = new ol.geom.MultiLineString([]);
var layerRoute = new ol.layer.Vector({
  source: new ol.source.Vector({
    features: [
      new ol.Feature({ geometry: multiLineString })
    ]
  }),
  style: [
    new ol.style.Style({
      stroke: new ol.style.Stroke({
          width: 3,
          color: [51, 51, 51, 1]
          //lineDash: [1, 5]
      }),
      zIndex: 2
    })
  ],
  updateWhileAnimating: true
});

var map = new ol.Map({
  target: document.getElementById('map'),
  loadTilesWhileAnimating: true,
  loadTilesWhileInteracting: true,
  view: olview,
  layers: [
    new ol.layer.Tile({
      style: 'Aerial',
      source: new ol.source.OSM()
    }),
    layerRoute, layerFeatures
  ]
});


var popup = new ol.Overlay.Popup;
popup.setOffset([0, -25]);
map.addOverlay(popup);


var fill = new ol.style.Fill({color:[255,255,255,1]}),
    stroke = new ol.style.Stroke({color:[0,0,0,1]});

var style_parada = [
  new ol.style.Style({
    image: new ol.style.Icon(({
      scale: .6, anchor: [0.5, 1],
      src: '//raw.githubusercontent.com/jonataswalker/map-utils/master/images/marker.png'
    })),
    zIndex: 5
  }),
  new ol.style.Style({
    image: new ol.style.Circle({
      radius: 5, fill: fill, stroke: stroke
    }),
    zIndex: 4
  })
  
];
var style_cluster = [
  new ol.style.Style({
    image: new ol.style.Icon(({
      scale: .8, anchor: [0.51, 1],
      src: '//raw.githubusercontent.com/jonataswalker/map-utils/master/images/marker-cluster.png'
    })),
    zIndex: 15
  }),
  new ol.style.Style({
    image: new ol.style.Circle({
      radius: 7, fill: fill, stroke: stroke
    }),
    zIndex: 14
  })
];
var style_cluster_hover = [
  new ol.style.Style({
    image: new ol.style.Circle({
      radius: 7, stroke: stroke,
      fill: new ol.style.Fill({color:[46,52,54,1]})
    }),
    zIndex: 1
  })
];
var coord__ = [-8347855.57, -679521.35];
var feature2 = new ol.Feature({
    type: 'cluster',
    children: [1,2,3],
    coord: coord__,
    expanded: false,
    geometry: new ol.geom.Point(coord__)
});
var feature3 = new ol.Feature({
    type: 'click',
    desc: 'click1',
    coord: coord__,
    geometry: new ol.geom.Point(coord__)
});
var feature4 = new ol.Feature({
    type: 'click',
    desc: 'click2',
    coord: coord__,
    geometry: new ol.geom.Point(coord__)
});
var feature5 = new ol.Feature({
    type: 'click',
    desc: 'click3',
    coord: coord__,
    geometry: new ol.geom.Point(coord__)
});
feature3.setId(1);
feature4.setId(2);
feature5.setId(3);


feature2.setStyle(style_cluster);
sourceFeatures.addFeatures([feature2, feature3, feature4, feature5]);

map.on('click', function(evt) {
  popup.hide();
  
  var f = map.forEachFeatureAtPixel(evt.pixel, function(f){return f;});
  if (f && f.get('type') == 'click') {
      var geometry = f.getGeometry();
      var coord = geometry.getCoordinates();
      
      popup.show(coord, f.get('desc'));
      
  } else{
      
    sourceFeatures.forEachFeature(function(ft){
      if(ft.get('type') == 'cluster' && ft.get('expanded') === true){
        var children = ft.get('children'), child, c_coord;
        children.forEach(function(id){
          child = sourceFeatures.getFeatureById(id);
          
          c_coord = child.get('coord');
          
          child.setStyle(null);
          child.setGeometry(new ol.geom.Point(c_coord));
          
          ft.set('expanded', false);
          ft.setStyle(style_cluster);
          multiLineString.setCoordinates([]);
        });
      }
    });
  }
});

map.on('pointermove', function(e) {
  if (e.dragging) {
      return;
  }

  var pixel = map.getEventPixel(e.originalEvent);
  var hit = map.hasFeatureAtPixel(pixel);
  
  map.getTarget().style.cursor = hit ? 'pointer' : '';

  if (hit) displayOverlapping(pixel);
});

var displayOverlapping = function(pixel) {

  var f = map.forEachFeatureAtPixel(pixel, function(ft, l) {
      return ft;
  });
  if (f && f.get('type') == 'cluster') {
      
    if(f.get('expanded') === true) return;
    
    var geom = f.getGeometry(),
        coord = geom.getCoordinates(),
        px = map.getPixelFromCoordinate(coord),
        extent = [coord[0], coord[1], coord[0], coord[1]],
        ar_features = [];
        


    sourceFeatures.forEachFeatureInExtent(extent, function(ft){
        if(ft.get('type') == 'click') ar_features.push(ft);
    });
    
    var points = generatePointsCircle(ar_features.length, px);

    f.set('expanded', true);
    f.setStyle(style_cluster_hover);
    multiLineString.setCoordinates([]);
    
    ar_features.forEach(function(row, index){
      var cd_end = map.getCoordinateFromPixel(points[index]);
      
      multiLineString.appendLineString(
          new ol.geom.LineString([coord, cd_end])
      );
      
      row.setGeometry(new ol.geom.Point(cd_end));
      row.setStyle(style_parada);
    });
  }
};
#map{
    position:absolute;
    z-index:1;
    width:100%; height:100%;
    top:0; bottom:0;
}
.ol-popup {
    position: absolute;
    background-color: white;
    -webkit-filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
    filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
    padding: 15px;
    border-radius: 10px;
    border: 1px solid #cccccc;
    bottom: 12px;
    left: -50px;
}
.ol-popup:after, .ol-popup:before {
    top: 100%;
    border: solid transparent;
    content: " ";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
}
.ol-popup:after {
    border-top-color: white;
    border-width: 10px;
    left: 48px;
    margin-left: -10px;
}
.ol-popup:before {
    border-top-color: #cccccc;
    border-width: 11px;
    left: 48px;
    margin-left: -11px;
}
.ol-popup-content {
    position: relative;
    min-width: 200px;
    min-height: 150px;
    height: 100%;
    max-height: 250px;
    padding:2px;
    white-space: normal;        
    background-color: #f7f7f9;
    border: 1px solid #e1e1e8;
    overflow-y: auto;
    overflow-x: hidden;
}
.ol-popup-content p{
    font-size: 14px;
    padding: 2px 4px;
    color: #222;
    margin-bottom: 15px;
}
.ol-popup-closer {
    position: absolute;
    top: -4px;
    right: 2px;
    font-size: 100%;
    color: #0088cc;
    text-decoration: none;
}
a.ol-popup-closer:hover{
    color: #005580;
    text-decoration: underline;
}
.ol-popup-closer:after {
    content: "✖";
}
/**
 * OpenLayers 3 Popup Overlay.
 * See [the examples](./examples) for usage. Styling can be done via CSS.
 * @constructor
 * @extends {ol.Overlay}
 * @param {Object} opt_options Overlay options, extends olx.OverlayOptions adding:
 *                              **`panMapIfOutOfView`** `Boolean` - Should the
 *                              map be panned so that the popup is entirely
 *                              within view.
 */
ol.Overlay.Popup = function(opt_options) {

    var options = opt_options || {};

    this.panMapIfOutOfView = options.panMapIfOutOfView;
    if (this.panMapIfOutOfView === undefined) {
        this.panMapIfOutOfView = true;
    }

    this.ani = options.ani;
    if (this.ani === undefined) {
        this.ani = ol.animation.pan;
    }

    this.ani_opts = options.ani_opts;
    if (this.ani_opts === undefined) {
        this.ani_opts = {'duration': 250};
    }

    this.container = document.createElement('div');
    this.container.className = 'ol-popup';

    this.closer = document.createElement('a');
    this.closer.className = 'ol-popup-closer';
    this.closer.href = '#';
    this.container.appendChild(this.closer);

    var that = this;
    this.closer.addEventListener('click', function(evt) {
        that.container.style.display = 'none';
        that.closer.blur();
        evt.preventDefault();
    }, false);

    this.content = document.createElement('div');
    this.content.className = 'ol-popup-content';
    this.content.id = 'ol-popup-content';
    this.container.appendChild(this.content);

    ol.Overlay.call(this, {
        element: this.container,
        stopEvent: true
    });

};

ol.inherits(ol.Overlay.Popup, ol.Overlay);

/**
 * Show the popup.
 * @param {ol.Coordinate} coord Where to anchor the popup.
 * @param {String} html String of HTML to display within the popup.
 */
ol.Overlay.Popup.prototype.show = function(coord, html) {
    this.setPosition(coord);
    this.content.innerHTML = html;
    this.container.style.display = 'block';

    window.setTimeout(function(){
        document.getElementById('ol-popup-content').scrollTop = 0;
    }, 100);
    
    if (this.panMapIfOutOfView) {
        this.panIntoView_(coord);
    }
    return this;
};

/**
 * @private
 */
ol.Overlay.Popup.prototype.panIntoView_ = function(coord) {

    var popSize = {
            width: this.getElement().clientWidth + 20,
            height: this.getElement().clientHeight + 20
        },
        mapSize = this.getMap().getSize();

    var tailHeight = 20,
        tailOffsetLeft = 60,
        tailOffsetRight = popSize.width - tailOffsetLeft,
        popOffset = this.getOffset(),
        popPx = this.getMap().getPixelFromCoordinate(coord);

    var fromLeft = (popPx[0] - tailOffsetLeft),
        fromRight = mapSize[0] - (popPx[0] + tailOffsetRight);

    var fromTop = popPx[1] - popSize.height + popOffset[1],
        fromBottom = mapSize[1] - (popPx[1] + tailHeight) - popOffset[1];

    var center = this.getMap().getView().getCenter(),
        px = this.getMap().getPixelFromCoordinate(center);

    if (fromRight < 0) {
        px[0] -= fromRight;
    } else if (fromLeft < 0) {
        px[0] += fromLeft;
    }
    
    if (fromTop < 0) {
        //px[1] = 170 + fromTop;
        px[1] += fromTop; //original
    } else if (fromBottom < 0) {
        px[1] -= fromBottom;
    }

    if (this.ani && this.ani_opts) {
        this.ani_opts.source = center;
        this.getMap().beforeRender(this.ani(this.ani_opts));
    }
    this.getMap().getView().setCenter(this.getMap().getCoordinateFromPixel(px));

    return this.getMap().getView().getCenter();

};

/**
 * Hide the popup.
 */
ol.Overlay.Popup.prototype.hide = function() {
    this.container.style.display = 'none';
    return this;
};