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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions