/** * @module ol/renderer/vector */ import ImageState from '../ImageState.js'; import {getUid} from '../util.js'; /** * Feature callback. The callback will be called with three arguments. The first * argument is one {@link module:ol/Feature~Feature feature} or {@link module:ol/render/Feature~RenderFeature render feature} * at the pixel, the second is the {@link module:ol/layer/Layer~Layer layer} of the feature and will be null for * unmanaged layers. The third is the {@link module:ol/geom/SimpleGeometry~SimpleGeometry} of the feature. For features * with a GeometryCollection geometry, it will be the first detected geometry from the collection. * @template T * @typedef {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default, import("../geom/SimpleGeometry.js").default): T} FeatureCallback */ /** * Tolerance for geometry simplification in device pixels. * @type {number} */ const SIMPLIFY_TOLERANCE = 0.5; /** * @const * @type {Object} */ const GEOMETRY_RENDERERS = { 'Point': renderPointGeometry, 'LineString': renderLineStringGeometry, 'Polygon': renderPolygonGeometry, 'MultiPoint': renderMultiPointGeometry, 'MultiLineString': renderMultiLineStringGeometry, 'MultiPolygon': renderMultiPolygonGeometry, 'GeometryCollection': renderGeometryCollectionGeometry, 'Circle': renderCircleGeometry, }; /** * @param {import("../Feature.js").FeatureLike} feature1 Feature 1. * @param {import("../Feature.js").FeatureLike} feature2 Feature 2. * @return {number} Order. */ export function defaultOrder(feature1, feature2) { return parseInt(getUid(feature1), 10) - parseInt(getUid(feature2), 10); } /** * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @return {number} Squared pixel tolerance. */ export function getSquaredTolerance(resolution, pixelRatio) { const tolerance = getTolerance(resolution, pixelRatio); return tolerance * tolerance; } /** * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @return {number} Pixel tolerance. */ export function getTolerance(resolution, pixelRatio) { return (SIMPLIFY_TOLERANCE * resolution) / pixelRatio; } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Builder group. * @param {import("../geom/Circle.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderCircleGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { const circleReplay = builderGroup.getBuilder(style.getZIndex(), 'Circle'); circleReplay.setFillStrokeStyle(fillStyle, strokeStyle); circleReplay.drawCircle(geometry, feature); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { const textReplay = (declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), 'Text' ); textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. * @param {function(import("../events/Event.js").default): void} listener Listener function. * @param {import("../proj.js").TransformFunction} [transform] Transform from user to view projection. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. * @return {boolean} `true` if style is loading. */ export function renderFeature( replayGroup, feature, style, squaredTolerance, listener, transform, declutterBuilderGroup ) { let loading = false; const imageStyle = style.getImage(); if (imageStyle) { const imageState = imageStyle.getImageState(); if (imageState == ImageState.LOADED || imageState == ImageState.ERROR) { imageStyle.unlistenImageChange(listener); } else { if (imageState == ImageState.IDLE) { imageStyle.load(); } imageStyle.listenImageChange(listener); loading = true; } } renderFeatureInternal( replayGroup, feature, style, squaredTolerance, transform, declutterBuilderGroup ); return loading; } /** * @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. * @param {import("../proj.js").TransformFunction} [transform] Optional transform function. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderFeatureInternal( replayGroup, feature, style, squaredTolerance, transform, declutterBuilderGroup ) { const geometry = style.getGeometryFunction()(feature); if (!geometry) { return; } const simplifiedGeometry = geometry.simplifyTransformed( squaredTolerance, transform ); const renderer = style.getRenderer(); if (renderer) { renderGeometry(replayGroup, simplifiedGeometry, style, feature); } else { const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()]; geometryRenderer( replayGroup, simplifiedGeometry, style, feature, declutterBuilderGroup ); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../geom/Geometry.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ function renderGeometry(replayGroup, geometry, style, feature) { if (geometry.getType() == 'GeometryCollection') { const geometries = /** @type {import("../geom/GeometryCollection.js").default} */ ( geometry ).getGeometries(); for (let i = 0, ii = geometries.length; i < ii; ++i) { renderGeometry(replayGroup, geometries[i], style, feature); } return; } const replay = replayGroup.getBuilder(style.getZIndex(), 'Default'); replay.drawCustom( /** @type {import("../geom/SimpleGeometry.js").default} */ (geometry), feature, style.getRenderer(), style.getHitDetectionRenderer() ); } /** * @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderGeometryCollectionGeometry( replayGroup, geometry, style, feature, declutterBuilderGroup ) { const geometries = geometry.getGeometriesArray(); let i, ii; for (i = 0, ii = geometries.length; i < ii; ++i) { const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()]; geometryRenderer( replayGroup, geometries[i], style, feature, declutterBuilderGroup ); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderLineStringGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const strokeStyle = style.getStroke(); if (strokeStyle) { const lineStringReplay = builderGroup.getBuilder( style.getZIndex(), 'LineString' ); lineStringReplay.setFillStrokeStyle(null, strokeStyle); lineStringReplay.drawLineString(geometry, feature); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { const textReplay = (declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), 'Text' ); textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderMultiLineStringGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const strokeStyle = style.getStroke(); if (strokeStyle) { const lineStringReplay = builderGroup.getBuilder( style.getZIndex(), 'LineString' ); lineStringReplay.setFillStrokeStyle(null, strokeStyle); lineStringReplay.drawMultiLineString(geometry, feature); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { const textReplay = (declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), 'Text' ); textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderMultiPolygonGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (strokeStyle || fillStyle) { const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon'); polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle); polygonReplay.drawMultiPolygon(geometry, feature); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { const textReplay = (declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), 'Text' ); textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderPointGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const imageStyle = style.getImage(); const textStyle = style.getText(); /** @type {import("../render/canvas.js").DeclutterImageWithText} */ let declutterImageWithText; if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; } let imageBuilderGroup = builderGroup; if (declutterBuilderGroup) { const declutterMode = imageStyle.getDeclutterMode(); if (declutterMode !== 'none') { imageBuilderGroup = declutterBuilderGroup; if (declutterMode === 'obstacle') { // draw in non-declutter group: const imageReplay = builderGroup.getBuilder( style.getZIndex(), 'Image' ); imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawPoint(geometry, feature); } else if (textStyle && textStyle.getText()) { declutterImageWithText = {}; } } } const imageReplay = imageBuilderGroup.getBuilder( style.getZIndex(), 'Image' ); imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawPoint(geometry, feature); } if (textStyle && textStyle.getText()) { let textBuilderGroup = builderGroup; if (declutterBuilderGroup) { textBuilderGroup = declutterBuilderGroup; } const textReplay = textBuilderGroup.getBuilder(style.getZIndex(), 'Text'); textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderMultiPointGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const imageStyle = style.getImage(); const textStyle = style.getText(); /** @type {import("../render/canvas.js").DeclutterImageWithText} */ let declutterImageWithText; if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; } let imageBuilderGroup = builderGroup; if (declutterBuilderGroup) { const declutterMode = imageStyle.getDeclutterMode(); if (declutterMode !== 'none') { imageBuilderGroup = declutterBuilderGroup; if (declutterMode === 'obstacle') { // draw in non-declutter group: const imageReplay = builderGroup.getBuilder( style.getZIndex(), 'Image' ); imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawMultiPoint(geometry, feature); } else if (textStyle && textStyle.getText()) { declutterImageWithText = {}; } } } const imageReplay = imageBuilderGroup.getBuilder( style.getZIndex(), 'Image' ); imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawMultiPoint(geometry, feature); } if (textStyle && textStyle.getText()) { let textBuilderGroup = builderGroup; if (declutterBuilderGroup) { textBuilderGroup = declutterBuilderGroup; } const textReplay = textBuilderGroup.getBuilder(style.getZIndex(), 'Text'); textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } } /** * @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering. */ function renderPolygonGeometry( builderGroup, geometry, style, feature, declutterBuilderGroup ) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon'); polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle); polygonReplay.drawPolygon(geometry, feature); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { const textReplay = (declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), 'Text' ); textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } }