Search code examples
openlayers

OpenLayers: How can I stop a click from causing a selection when it shouldn't be?


To reproduce, click on the blue marker containing the text 4. Because this is a cluster marker and all of the markers it is clustering are at the same coordinate, it does not make sense to try to zoom in. Instead of zooming in, I center the map on the marker.

However, this click on the 4 marker, is causing a click on the big rectangle to the left, which results in a Select being triggered for the rectangle. This, of course, should not happen.

I have tried telling the event to not propagate if I am doing the centering operation, but that is not helping.

What do I need to change so the Select is not triggered?

function createRectangle(polygons, layerName) {
  const features = [];

  for (const [i, polygon] of polygons.entries()) {
    const feature = {
      type: "Feature",
      geometry: {
        type: "Polygon",
        coordinates: polygon
      },
      properties: {
        id: `${layerName} index ${i}`,
        layerName
      }
    };

    features.push(feature);
  }

  const json = {
    type: "FeatureCollection",
    features
  };

  return json;
}

function createPoints(points, groupName) {
  const features = [];

  for (const [i, point] of points.entries()) {
    const feature = {
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: point
      },
      properties: {
        id: `${groupName} index ${i}`,
        groupName
      }
    };

    features.push(feature);
  }

  const json = {
    type: "FeatureCollection",
    features
  };

  return json;
}

const thePoints = createPoints(
  [
    [50, 33],
    [50, 33],
    [50, 33],
    [50, 33]
  ],
  "thePoints"
);

const styles = {
  Point: new ol.style.Style({
    image: new ol.style.Circle({
      radius: 7,
      fill: new ol.style.Fill({ color: "red" })
    }),
    stroke: new ol.style.Stroke({
      color: "hsla(0, 50%, 100%, 1.0)",
      width: 5
    }),
    fill: new ol.style.Fill({
      color: "hsla(0, 50%, 50%, 1.0)"
    })
  }),
  Polygon: new ol.style.Style({
    stroke: new ol.style.Stroke({
      color: "hsla(0, 50%, 100%, 1.0)",
      width: 5
    }),
    fill: new ol.style.Fill({
      color: "hsla(0, 50%, 50%, 1.0)"
    })
  })
};

const styleFunction = function (feature) {
  const featureType = feature.getGeometry().getType();
  return styles[featureType];
};

const vectorSource = new ol.source.Vector({
  features: new ol.format.GeoJSON({
    featureProjection: "EPSG:4326" // 4326 3857
  }).readFeatures(thePoints)
});

const cluster = new ol.source.Cluster({
  distance: 30,
  minDistance: 10,
  source: vectorSource
});

const styleCache = {};
const vectorLayer = new ol.layer.Vector({
  source: cluster,
  style: function (feature) {
    const size = feature.get("features").length;
    let style = styleCache[size];
    if (!style) {
      style = new ol.style.Style({
        image: new ol.style.Circle({
          radius: 10,
          stroke: new ol.style.Stroke({
            color: "#fff"
          }),
          fill: new ol.style.Fill({
            color: "#3399CC"
          })
        }),
        text: new ol.style.Text({
          text: size.toString(),
          fill: new ol.style.Fill({
            color: "#fff"
          })
        })
      });
      styleCache[size] = style;
    }
    return style;
  },
  zIndex: 5,
  properties: {
    name: "big"
  }
});

const rectangles = createRectangle(
  [
    [
      [
        [-120, -75],
        [-120, 75],
        [45, 75],
        [45, -75]
      ]
    ]
  ],
  "rectangle"
);

const rectangleSource = new ol.source.Vector({
  features: new ol.format.GeoJSON({
    featureProjection: "EPSG:4326" // 4326 3857
  }).readFeatures(rectangles)
});

const rectangleLayer = new ol.layer.Vector({
  source: rectangleSource,
  style: styleFunction,
  zIndex: 5
});

const map = new ol.Map({
  target: "map",
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM()
    }),
    vectorLayer,
    rectangleLayer
  ],
  view: new ol.View({
    projection: "EPSG:4326",
    center: [179, 0],
    zoom: 1
  })
});

map.on("click", (e) => {
  vectorLayer.getFeatures(e.pixel).then((clickedFeatures) => {
    if (clickedFeatures.length) {
      // Get clustered Coordinates
      const features = clickedFeatures[0].get("features");
      if (features.length > 1) {
        e.stopPropagation();

        const extent = ol.extent.boundingExtent(
          features.map((r) => r.getGeometry().getCoordinates())
        );
        const extentSize = ol.extent.getSize(extent);
        const isSmallExtent = extentSize[0] === 0 && extentSize[1] === 0;

        console.log("isSmallExtent", isSmallExtent);

        if (isSmallExtent) {
          const viewport = map.getViewport();
          const boundingRect = viewport.getBoundingClientRect();

          console.log("boundingRect", boundingRect);

          const centerPoint = [
            (boundingRect.left + boundingRect.right) / 2,
            (boundingRect.top + boundingRect.bottom) / 2
          ];

          console.log("centerPoint", centerPoint);

          map
            .getView()
            .centerOn(ol.extent.getCenter(extent), map.getSize(), centerPoint);
        } else {
          map
            .getView()
            .fit(extent, { duration: 1000, padding: [50, 50, 50, 50] });
        }
      }
    }
  });
});

const selected = new ol.style.Style({
  fill: new ol.style.Fill({
    color: "hsla(270, 50%, 50%, .1)"
  }),
  stroke: new ol.style.Stroke({
    color: "hsla(270, 50%, 50%, 1)",
    width: 5
  })
});

const selectSingleClick = new ol.interaction.Select({
  style: (feature) => {
    const color = "hsla(270, 50%, 50%, .1)";
    selected.getFill().setColor(color);
    return selected;
  },
  layers: [rectangleLayer],
  condition: function (e) {
    return (
      ol.events.condition.singleClick(e) &&
      map.getFeaturesAtPixel(e.pixel, {
        layerFilter: function (l) {
          return l.getZIndex() > vectorLayer.getZIndex();
        }
      }).length === 0
    );
  }
});

selectSingleClick.on("select", function (e) {
  console.log("selectSingleClick onSelect");
});

map.addInteraction(selectSingleClick);
#map {
  height: 512px;
  width: 1024px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/ol.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/dist/ol.min.js"></script>

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


Solution

  • You could simply deactivate the select interaction for the duration of a single click (which fires 500ms after the click to ensure it is not a double click)

        selectSingleClick.setActive(false);
        setTimeout(() => { selectSingleClick.setActive(true); }, 600);
    

    function createRectangle(polygons, layerName) {
      const features = [];
    
      for (const [i, polygon] of polygons.entries()) {
        const feature = {
          type: "Feature",
          geometry: {
            type: "Polygon",
            coordinates: polygon
          },
          properties: {
            id: `${layerName} index ${i}`,
            layerName
          }
        };
    
        features.push(feature);
      }
    
      const json = {
        type: "FeatureCollection",
        features
      };
    
      return json;
    }
    
    function createPoints(points, groupName) {
      const features = [];
    
      for (const [i, point] of points.entries()) {
        const feature = {
          type: "Feature",
          geometry: {
            type: "Point",
            coordinates: point
          },
          properties: {
            id: `${groupName} index ${i}`,
            groupName
          }
        };
    
        features.push(feature);
      }
    
      const json = {
        type: "FeatureCollection",
        features
      };
    
      return json;
    }
    
    const thePoints = createPoints(
      [
        [50, 33],
        [50, 33],
        [50, 33],
        [50, 33]
      ],
      "thePoints"
    );
    
    const styles = {
      Point: new ol.style.Style({
        image: new ol.style.Circle({
          radius: 7,
          fill: new ol.style.Fill({ color: "red" })
        }),
        stroke: new ol.style.Stroke({
          color: "hsla(0, 50%, 100%, 1.0)",
          width: 5
        }),
        fill: new ol.style.Fill({
          color: "hsla(0, 50%, 50%, 1.0)"
        })
      }),
      Polygon: new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: "hsla(0, 50%, 100%, 1.0)",
          width: 5
        }),
        fill: new ol.style.Fill({
          color: "hsla(0, 50%, 50%, 1.0)"
        })
      })
    };
    
    const styleFunction = function (feature) {
      const featureType = feature.getGeometry().getType();
      return styles[featureType];
    };
    
    const vectorSource = new ol.source.Vector({
      features: new ol.format.GeoJSON({
        featureProjection: "EPSG:4326" // 4326 3857
      }).readFeatures(thePoints)
    });
    
    const cluster = new ol.source.Cluster({
      distance: 30,
      minDistance: 10,
      source: vectorSource
    });
    
    const styleCache = {};
    const vectorLayer = new ol.layer.Vector({
      source: cluster,
      style: function (feature) {
        const size = feature.get("features").length;
        let style = styleCache[size];
        if (!style) {
          style = new ol.style.Style({
            image: new ol.style.Circle({
              radius: 10,
              stroke: new ol.style.Stroke({
                color: "#fff"
              }),
              fill: new ol.style.Fill({
                color: "#3399CC"
              })
            }),
            text: new ol.style.Text({
              text: size.toString(),
              fill: new ol.style.Fill({
                color: "#fff"
              })
            })
          });
          styleCache[size] = style;
        }
        return style;
      },
      zIndex: 5,
      properties: {
        name: "big"
      }
    });
    
    const rectangles = createRectangle(
      [
        [
          [
            [-120, -75],
            [-120, 75],
            [45, 75],
            [45, -75]
          ]
        ]
      ],
      "rectangle"
    );
    
    const rectangleSource = new ol.source.Vector({
      features: new ol.format.GeoJSON({
        featureProjection: "EPSG:4326" // 4326 3857
      }).readFeatures(rectangles)
    });
    
    const rectangleLayer = new ol.layer.Vector({
      source: rectangleSource,
      style: styleFunction,
      zIndex: 5
    });
    
    const map = new ol.Map({
      target: "map",
      layers: [
        new ol.layer.Tile({
          source: new ol.source.OSM()
        }),
        vectorLayer,
        rectangleLayer
      ],
      view: new ol.View({
        projection: "EPSG:4326",
        center: [179, 0],
        zoom: 1
      })
    });
    
    let selectSingleClick;
    map.on("click", (e) => {
      vectorLayer.getFeatures(e.pixel).then((clickedFeatures) => {
        if (clickedFeatures.length) {
          // Get clustered Coordinates
          const features = clickedFeatures[0].get("features");
          if (features.length > 1) {
            selectSingleClick.setActive(false);
            setTimeout(() => { selectSingleClick.setActive(true); }, 600);
    
            const extent = ol.extent.boundingExtent(
              features.map((r) => r.getGeometry().getCoordinates())
            );
            const extentSize = ol.extent.getSize(extent);
            const isSmallExtent = extentSize[0] === 0 && extentSize[1] === 0;
    
            console.log("isSmallExtent", isSmallExtent);
    
            if (isSmallExtent) {
              const viewport = map.getViewport();
              const boundingRect = viewport.getBoundingClientRect();
    
              console.log("boundingRect", boundingRect);
    
              const centerPoint = [
                (boundingRect.left + boundingRect.right) / 2,
                (boundingRect.top + boundingRect.bottom) / 2
              ];
    
              console.log("centerPoint", centerPoint);
    
              map
                .getView()
                .centerOn(ol.extent.getCenter(extent), map.getSize(), centerPoint);
            } else {
              map
                .getView()
                .fit(extent, { duration: 1000, padding: [50, 50, 50, 50] });
            }
          }
        }
      });
    });
    
    const selected = new ol.style.Style({
      fill: new ol.style.Fill({
        color: "hsla(270, 50%, 50%, .1)"
      }),
      stroke: new ol.style.Stroke({
        color: "hsla(270, 50%, 50%, 1)",
        width: 5
      })
    });
    
    selectSingleClick = new ol.interaction.Select({
      style: (feature) => {
        const color = "hsla(270, 50%, 50%, .1)";
        selected.getFill().setColor(color);
        return selected;
      },
      layers: [rectangleLayer],
      condition: function (e) {
        return (
          ol.events.condition.singleClick(e) &&
          map.getFeaturesAtPixel(e.pixel, {
            layerFilter: function (l) {
              return l.getZIndex() > vectorLayer.getZIndex();
            }
          }).length === 0
        );
      }
    });
    
    selectSingleClick.on("select", function (e) {
      console.log("selectSingleClick onSelect");
    });
    
    map.addInteraction(selectSingleClick);
    #map {
      height: 512px;
      width: 1024px;
    }
    <link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/ol.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/dist/ol.min.js"></script>
    
    <div id="map"></div>

    The use of setTimeout could be avoided by using a clickHandled boolean set only when a map click causes some action, and its value being tested in the select condition:

    function createRectangle(polygons, layerName) {
      const features = [];
    
      for (const [i, polygon] of polygons.entries()) {
        const feature = {
          type: "Feature",
          geometry: {
            type: "Polygon",
            coordinates: polygon
          },
          properties: {
            id: `${layerName} index ${i}`,
            layerName
          }
        };
    
        features.push(feature);
      }
    
      const json = {
        type: "FeatureCollection",
        features
      };
    
      return json;
    }
    
    function createPoints(points, groupName) {
      const features = [];
    
      for (const [i, point] of points.entries()) {
        const feature = {
          type: "Feature",
          geometry: {
            type: "Point",
            coordinates: point
          },
          properties: {
            id: `${groupName} index ${i}`,
            groupName
          }
        };
    
        features.push(feature);
      }
    
      const json = {
        type: "FeatureCollection",
        features
      };
    
      return json;
    }
    
    const thePoints = createPoints(
      [
        [50, 33],
        [50, 33],
        [50, 33],
        [50, 33]
      ],
      "thePoints"
    );
    
    const styles = {
      Point: new ol.style.Style({
        image: new ol.style.Circle({
          radius: 7,
          fill: new ol.style.Fill({ color: "red" })
        }),
        stroke: new ol.style.Stroke({
          color: "hsla(0, 50%, 100%, 1.0)",
          width: 5
        }),
        fill: new ol.style.Fill({
          color: "hsla(0, 50%, 50%, 1.0)"
        })
      }),
      Polygon: new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: "hsla(0, 50%, 100%, 1.0)",
          width: 5
        }),
        fill: new ol.style.Fill({
          color: "hsla(0, 50%, 50%, 1.0)"
        })
      })
    };
    
    const styleFunction = function (feature) {
      const featureType = feature.getGeometry().getType();
      return styles[featureType];
    };
    
    const vectorSource = new ol.source.Vector({
      features: new ol.format.GeoJSON({
        featureProjection: "EPSG:4326" // 4326 3857
      }).readFeatures(thePoints)
    });
    
    const cluster = new ol.source.Cluster({
      distance: 30,
      minDistance: 10,
      source: vectorSource
    });
    
    const styleCache = {};
    const vectorLayer = new ol.layer.Vector({
      source: cluster,
      style: function (feature) {
        const size = feature.get("features").length;
        let style = styleCache[size];
        if (!style) {
          style = new ol.style.Style({
            image: new ol.style.Circle({
              radius: 10,
              stroke: new ol.style.Stroke({
                color: "#fff"
              }),
              fill: new ol.style.Fill({
                color: "#3399CC"
              })
            }),
            text: new ol.style.Text({
              text: size.toString(),
              fill: new ol.style.Fill({
                color: "#fff"
              })
            })
          });
          styleCache[size] = style;
        }
        return style;
      },
      zIndex: 5,
      properties: {
        name: "big"
      }
    });
    
    const rectangles = createRectangle(
      [
        [
          [
            [-120, -75],
            [-120, 75],
            [45, 75],
            [45, -75]
          ]
        ]
      ],
      "rectangle"
    );
    
    const rectangleSource = new ol.source.Vector({
      features: new ol.format.GeoJSON({
        featureProjection: "EPSG:4326" // 4326 3857
      }).readFeatures(rectangles)
    });
    
    const rectangleLayer = new ol.layer.Vector({
      source: rectangleSource,
      style: styleFunction,
      zIndex: 5
    });
    
    const map = new ol.Map({
      target: "map",
      layers: [
        new ol.layer.Tile({
          source: new ol.source.OSM()
        }),
        vectorLayer,
        rectangleLayer
      ],
      view: new ol.View({
        projection: "EPSG:4326",
        center: [179, 0],
        zoom: 1
      })
    });
    
    let clickHandled = false;
    map.on("click", (e) => {
      vectorLayer.getFeatures(e.pixel).then((clickedFeatures) => {
        clickHandled = false;
        if (clickedFeatures.length) {
          // Get clustered Coordinates
          const features = clickedFeatures[0].get("features");
          if (features.length > 1) {
            clickHandled = true;
    
            const extent = ol.extent.boundingExtent(
              features.map((r) => r.getGeometry().getCoordinates())
            );
            const extentSize = ol.extent.getSize(extent);
            const isSmallExtent = extentSize[0] === 0 && extentSize[1] === 0;
    
            console.log("isSmallExtent", isSmallExtent);
    
            if (isSmallExtent) {
              const viewport = map.getViewport();
              const boundingRect = viewport.getBoundingClientRect();
    
              console.log("boundingRect", boundingRect);
    
              const centerPoint = [
                (boundingRect.left + boundingRect.right) / 2,
                (boundingRect.top + boundingRect.bottom) / 2
              ];
    
              console.log("centerPoint", centerPoint);
    
              map
                .getView()
                .centerOn(ol.extent.getCenter(extent), map.getSize(), centerPoint);
            } else {
              map
                .getView()
                .fit(extent, { duration: 1000, padding: [50, 50, 50, 50] });
            }
          }
        }
      });
    });
    
    const selected = new ol.style.Style({
      fill: new ol.style.Fill({
        color: "hsla(270, 50%, 50%, .1)"
      }),
      stroke: new ol.style.Stroke({
        color: "hsla(270, 50%, 50%, 1)",
        width: 5
      })
    });
    
    selectSingleClick = new ol.interaction.Select({
      style: (feature) => {
        const color = "hsla(270, 50%, 50%, .1)";
        selected.getFill().setColor(color);
        return selected;
      },
      layers: [rectangleLayer],
      condition: function (e) {
        return (
          !clickHandled &&
          ol.events.condition.singleClick(e) &&
          map.getFeaturesAtPixel(e.pixel, {
            layerFilter: function (l) {
              return l.getZIndex() > vectorLayer.getZIndex();
            }
          }).length === 0
        );
      }
    });
    
    selectSingleClick.on("select", function (e) {
      console.log("selectSingleClick onSelect");
    });
    
    map.addInteraction(selectSingleClick);
    #map {
      height: 512px;
      width: 1024px;
    }
    <link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/ol.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/7.3.0/dist/ol.min.js"></script>
    
    <div id="map"></div>