Skip to content

Attempting to add support for line-pattern by Vibe coding #1386

@lzqwebsoft

Description

@lzqwebsoft

In my current development work, I have a requirement to load a Mapbox Style using OpenLayers. However, after loading the map, I found that image-based line patterns are not supported. I searched for solutions online and reviewed the source code of ol-mapbox-style, ultimately locating the issue in src/stylefunction.js:

if (type != 1 && layer.type == 'line') {
	if (!('line-pattern' in paint)) {
		color = colorWithOpacity(
			getValue(
			layer,
			'paint',
			'line-color',
			f,
			functionCache,
			featureState,
			),
			getValue(
			layer,
			'paint',
			'line-opacity',
			f,
			functionCache,
			featureState,
			),
		);
	} else {
		color = undefined;  // line-pattern is not supported
	}
// ....

It turns out that line-pattern (image-based line styling) is intentionally unsupported here.
Based on the discussion in the related issue: line-pattern webgl#1045

I referenced the reply from @ahocevar

using a transform and doing separate stroke() operations for each segment.

Given the complexity and size of the source code, I didn't dive into the details directly. Instead, I used Claude Opus 4.5 to help me understand the problem and generate a patch to add line-pattern support. The AI did an excellent job of grasping my needs, and the solution leverages patch-package to apply the modification:

            // Remove color = undefined; and add :
            const lineIcon = getValue(
              layer,
              'paint',
              'line-pattern',
              f,
              functionCache,
              featureState,
            );
            if (lineIcon) {
              const icon =
                typeof lineIcon === 'string'
                  ? fromTemplate(lineIcon, properties)
                  : lineIcon.toString();
              const spriteImage = getSpriteImageForIcon(icon, spriteImages);
              if (spriteData && spriteData[icon] && spriteImage) {
                const lineOpacity = getValue(
                  layer,
                  'paint',
                  'line-opacity',
                  f,
                  functionCache,
                  featureState,
                );
                const lineWidth = getValue(
                  layer,
                  'paint',
                  'line-width',
                  f,
                  functionCache,
                  featureState,
                );
                const spriteImageData = spriteData[icon];

                // Create a cached pattern image canvas
                const icon_cache_key = icon + '.linepattern';
                let patternCanvas = patternCache[icon_cache_key];
                if (!patternCanvas) {
                  patternCanvas = createCanvas(
                    spriteImageData.width,
                    spriteImageData.height,
                  );
                  const pctx = patternCanvas.getContext('2d');
                  pctx.drawImage(
                    spriteImage.image,
                    spriteImageData.x,
                    spriteImageData.y,
                    spriteImageData.width,
                    spriteImageData.height,
                    0,
                    0,
                    spriteImageData.width,
                    spriteImageData.height,
                  );
                  patternCache[icon_cache_key] = patternCanvas;
                }

                // Create a custom renderer style for line-pattern
                const patternHeight = spriteImageData.height;
                const patternWidth = spriteImageData.width;
                const rendererOpacity = lineOpacity === undefined ? 1 : lineOpacity;

                ++stylesLength;
                style = new Style({
                  renderer: function (pixelCoordinates, state) {
                    const ctx = state.context;
                    const geometry = state.geometry;
                    const geomType = geometry.getType();

                    ctx.save();
                    ctx.globalAlpha = rendererOpacity;

                    // Get pixel coordinates array(s)
                    let coordArrays;
                    if (geomType === 'MultiLineString') {
                      coordArrays = pixelCoordinates;
                    } else {
                      coordArrays = [pixelCoordinates];
                    }

                    // Draw pattern along each line
                    for (let a = 0; a < coordArrays.length; a++) {
                      const coords = coordArrays[a];
                      if (!coords || coords.length < 2) continue;

                      for (let i = 0; i < coords.length - 1; i++) {
                        const start = coords[i];
                        const end = coords[i + 1];

                        const dx = end[0] - start[0];
                        const dy = end[1] - start[1];
                        const segmentLength = Math.sqrt(dx * dx + dy * dy);

                        if (segmentLength < 1) continue;

                        const angle = Math.atan2(dy, dx);

                        // Calculate how many pattern images fit in this segment
                        const numPatterns = Math.ceil(segmentLength / patternWidth);

                        for (let j = 0; j < numPatterns; j++) {
                          const t = (j * patternWidth) / segmentLength;
                          if (t > 1) break;

                          const x = start[0] + t * dx;
                          const y = start[1] + t * dy;

                          ctx.save();
                          ctx.translate(x, y);
                          ctx.rotate(angle);

                          // Draw the pattern image centered on the line
                          const drawWidth = Math.min(patternWidth, segmentLength - j * patternWidth);
                          ctx.drawImage(
                            patternCanvas,
                            0, 0, drawWidth, patternHeight,
                            0, -patternHeight / 2, drawWidth, patternHeight
                          );

                          ctx.restore();
                        }
                      }
                    }

                    ctx.restore();
                  },
                  zIndex: index,
                });
                styles[stylesLength] = style;

                // Skip the normal stroke rendering for this layer
                continue;
              }
            }

After implementing the change, I ran:

npx patch-package ol-mapbox-style

Then re-run the project with:

npx vite --force

To my delight, the image-based line patterns now render successfully!

From @ahocevar's comment, I understand that official line-pattern support is planned for a future release. Due to my limited technical expertise, I don't fully comprehend all the details of the AI-generated code. However, I wanted to share this working patch with the maintainers in the hope that it might help accelerate the official implementation process.

I welcome any discussions or feedback from the community. Thank you!

ol-mapbox-style+13.1.1.patch

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions