Search code examples
javascriptopenlayers

OpenLayer Icon user free transform


In my app I wrote an ol.interaction.Draw code that allow me to draw, rotate and scale proportionally an icon on a map view. After drawing the icon I can still scale it (but unfortunately I can't rotate it anymore) and drag and drop on the map but this process is not so much user friendly (e.g. it is difficult to understand where to position the mouse on the icon so to scale it/drag and drop).

So I need to emulate on my app the "Photoshop Ctrl+T" behavior: so clicking on my icon (or maybe with a key combination), it should appear all around a kind of rectangle that can allow me to rotate the icon, edit the base and high or scale proportionally (and of course this rectangle should disappear when I finish the editing). It would be possible and if yes how I can develope something like that?

This is my current code:

initMap: function () {
           
            var white = [255, 255, 255, 1];
            var blue = [0, 153, 255, 1];
            var width = 3;

            map = this.map;

            this.features = new ol.Collection();

            
            styles = [
              new ol.style.Style({

                image: new ol.style.Circle({
                  radius: width * 2,

                  fill: new ol.style.Fill({
                     color: 'rgba(255, 255, 255, 0.1)'
                  }),
                }),
              }),
            ];

            var treeStyle = new ol.style.Style({
                image: new ol.style.Icon({
                   src: 'https://www.mikenunn.net/data/oak-tree-icon-png-17.png',
                }),
            });

            styleFunction = function(feature, resolution) {
                if (feature.getGeometry().getCenter) {
                    treeStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
                    treeStyle.getImage().setRotation(feature.getGeometry().get('rotation'));
                    treeStyle.getImage().setScale(feature.getGeometry().getRadius()/(150*resolution));
                    return treeStyle;
                } else {
                   return styles;
                }
            }

            featureOverlay = new ol.layer.Vector({
                source: new ol.source.Vector({
                    features: this.features,
                    wrapX: false
                }),
                style: styleFunction
            });
            featureOverlay.setMap(map);

           

            this.draw = new ol.interaction.Draw({
                features: this.features,
                type: 'Circle',
                geometryFunction: function(coordinates, geometry) {
                    var center = coordinates[0];
                    var last = coordinates[1];
                    var dx = center[0] - last[0];
                    var dy = center[1] - last[1];
                    var radius = Math.sqrt(dx * dx + dy * dy);
                    var rotation = Math.PI - Math.atan2(dy, dx);
                    geometry = geometry || new ol.geom.Circle(center, radius);
                    geometry.setCenter(center);
                    geometry.setRadius(radius);
                    geometry.set('rotation', rotation);
                    return new ol.geom.Circle(center, radius);
                },
                style: styleFunction,
                handler: 'onSaveClick'
            });

            this.draw.on('drawstart', function () {
                        this.features.clear();
                    }, this);

            this.map.addInteraction(this.draw);
        }


Solution

  • Adding a pointermove select interaction with a style which shows the circle and its center will highlight where to use the modify interaction

      var white = [255, 255, 255, 1];
      var blue = [0, 153, 255, 1];
      var width = 3;
    
      var pointStyle = new ol.style.Style({
        image: new ol.style.Circle({
          radius: width * 2,
          fill: new ol.style.Fill({
            color: blue
          }),
          stroke: new ol.style.Stroke({
            color: white,
            width: width / 2
          })
        }),
        zIndex: Infinity
      });
    
      var selectStyles = [
          new ol.style.Style({
              fill: new ol.style.Fill({
                  color: [255, 255, 255, 0.5]
             })
          }),
          new ol.style.Style({
              stroke: new ol.style.Stroke({
                  color: white,
                  width: width + 2
              })
          }),
          new ol.style.Style({
              stroke: new ol.style.Stroke({
                  color: blue,
                  width: width
              })
          }),
          florplanStyle,
          pointStyle
      ];
    
      selectStyleFunction = function(feature, resolution) {
          if (feature.getGeometry().getCenter) {
              pointStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
              florplanStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
              florplanStyle.getImage().setRotation(feature.getGeometry().get('rotation'));
              florplanStyle.getImage().setScale(feature.getGeometry().getRadius()/(150*resolution));
              return selectStyles;
          } else {
              return styles;
          }
       }
    
       map.addInteraction(new ol.interaction.Select({
           condition: ol.events.condition.pointerMove,
           features: this.features,
           style: selectStyleFunction
       }));
    

    There seems to be a problem using select and draw interactions together with a features collection. It does work linking them to a source

            source = new ol.source.Vector({
                features: this.features,
                wrapX: false
            }),
    
            featureOverlay = new ol.layer.Vector({
                source: source,
                style: styleFunction
            });
    
            this.draw = new ol.interaction.Draw({
                source: source,
                type: 'Circle',
                ....
    
            this.draw.on('drawstart', function () {
                        source.clear();
                    }, this);
    
            map.addInteraction(new ol.interaction.Select({
                condition: ol.events.condition.pointerMove,
                source: source,
                style: selectStyleFunction
            }));
    

    Rotation and displaying rectangular geometry would need a custom interaction. Luckily the ol-ext Transform interaction https://viglino.github.io/ol-ext/examples/interaction/map.interaction.transform.html seems to do everything you need. The rotation will need to be moved from the geometry to the feature before the interaction can be used, that can be done on the drawend, and the style updated to use rotation from either. There's currently a strange effect if you rotate a circle feature, which I have patched in the snippet, but I have informed the ol-ext developer of the issue so hopefully that won't be need if he fixes it.

    // Patch because ol-ext is changing extent of circle during rotation
    ol.geom.Circle.prototype.rotate = function(){};
    
    var white = [255, 255, 255, 1];
    var blue = [0, 153, 255, 1];
    var width = 3;
    styles = [
      new ol.style.Style({
        fill: new ol.style.Fill({
          color: [255, 255, 255, 0.5]
        })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: white,
          width: width + 2
        })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: blue,
          width: width
        })
      }),
      new ol.style.Style({
        image: new ol.style.Circle({
          radius: width * 2,
          fill: new ol.style.Fill({
            color: blue
          }),
          stroke: new ol.style.Stroke({
            color: white,
            width: width / 2
          })
        }),
        zIndex: Infinity
      })
    ];
    
    var treeStyle = new ol.style.Style({
        image: new ol.style.Icon({
           src: 'https://www.freeiconspng.com/uploads/oak-tree-icon-png-17.png'
        })
    });
    
    styleFunction = function(feature, resolution) {
        if (feature.getGeometry().getCenter) {
            treeStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
            // get rotation from drawn feature or geometry
            treeStyle.getImage().setRotation(feature.get('rotation') || feature.getGeometry().get('rotation'));
            treeStyle.getImage().setScale(feature.getGeometry().getRadius()/(150*resolution));
            return treeStyle;
        } else {
           return styles;
        } 
    }
    
    var raster = new ol.layer.Tile({
        source: new ol.source.OSM()
    });
    
    var features = new ol.Collection();
    
    var source = new ol.source.Vector({wrapX: false, features: features});
    
    var vector = new ol.layer.Vector({
        source: source,
        style: styleFunction
    });
    
    var map = new ol.Map({
        layers: [raster, vector],
        target: 'map',
        view: new ol.View({
            center: [-11000000, 4600000],
            zoom: 4
        })
    });
    
    var draw = new ol.interaction.Draw({
        source: source,
        type: 'Circle',
        geometryFunction: function(coordinates, geometry) {
            var center = coordinates[0];
            var last = coordinates[1];
            var dx = center[0] - last[0];
            var dy = center[1] - last[1];
            var radius = Math.sqrt(dx * dx + dy * dy);
            var rotation = Math.PI - Math.atan2(dy, dx);
            geometry = geometry || new ol.geom.Circle(center, radius);
            geometry.setCenterAndRadius(center, radius);
            geometry.set('rotation', rotation);
            return geometry;
        },
        style: styleFunction
    });
    
    draw.on('drawstart', function () {
        //source.clear();
    });
    
    draw.on('drawend', function (evt) {
        // move rotation from geometry to drawn feature
        evt.feature.set('rotation', evt.feature.getGeometry().get('rotation'));
        evt.feature.getGeometry().unset('rotation');
    });
    
    map.addInteraction(draw);
    
    var modify = new ol.interaction.Transform({
        features: features
    });
    
    var startangle = 0;
    
    modify.on('rotatestart', function(e) {
        startangle = e.feature.get('rotation') || 0;
    });
    
    modify.on('rotating', function (e) {
        // Set angle attribute to be used on style !
        e.feature.set('rotation', startangle - e.angle);
    });
    
    modify.on('select', function(e) {
        draw.setActive(e.features.length == 0);
    });
    
    map.addInteraction(modify);
    <link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
    <script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
    <script src="https://viglino.github.io/ol-ext/dist/ol-ext.js"></script>
    <div id="map" class="map"></div>

    Example of dynamically creating scaled icons (bounding polygons shown to help debugging):

    // Patch because ol-ext is changing extent of circle during rotation
    ol.geom.Circle.prototype.rotate = function(angle, anchor){
        var point = new ol.geom.Point(this.getCenter());
        point.rotate(angle, anchor);
        this.setCenter(point.getCoordinates());
    };
    
    var img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = 'https://www.mikenunn.net/data/oak-tree-icon-png-17.png';
    
    function getImage(img, scaleX, scaleY) {
        var canvas = document.createElement('canvas');
        var x = Math.round(img.width * scaleX);
        var y = Math.round(img.height * scaleY);
        canvas.width = x;
        canvas.height = y;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, x, y);
        var url = canvas.toDataURL();
        canvas.remove();
        return url;
    }
    
    var white = [255, 255, 255, 1];
    var blue = [0, 153, 255, 1];
    var width = 3;
    styles = [
      new ol.style.Style({
       //     fill: new ol.style.Fill({
       //       color: [255, 255, 255, 0.5]
       //     })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: white,
          width: width + 2
        })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: blue,
          width: width
        })
      }),
      new ol.style.Style({
        image: new ol.style.Circle({
          radius: width * 2,
          fill: new ol.style.Fill({
            color: blue
          }),
          stroke: new ol.style.Stroke({
            color: white,
            width: width / 2
          })
        }),
        zIndex: Infinity
      })
    ];
    
    var treeStyle = new ol.style.Style({
        image: new ol.style.Icon({
           src: img.src
        })
    });
    
    styleFunction = function(feature, resolution) {
        var resAdjust = 150 * resolution;
        var rotation = feature.get('rotation');
        if (rotation !== undefined) {
    
            var extent = feature.getGeometry().getExtent();
            var coordinates = feature.getGeometry().getCoordinates()[0];
            var tl = coordinates[0];
            var bl = coordinates[1];
            var br = coordinates[2];
            var tr = coordinates[3];
    
            var center = ol.extent.getCenter(extent);
            var top = new ol.geom.LineString([tl, tr]).getClosestPoint(center);
            var left = new ol.geom.LineString([tl, bl]).getClosestPoint(center);
    
            var dx = center[0] - left[0];
            var dy = center[1] - left[1];
            var scaleX = Math.sqrt(dx * dx + dy * dy)/resAdjust;
    
            var dx = top[0] - center[0];
            var dy = top[1] - center[1];
            var scaleY = Math.sqrt(dx * dx + dy * dy)/resAdjust;
    
            var treeStyle2 = new ol.style.Style({
                geometry: new ol.geom.Point(center),
                image: new ol.style.Icon({
                   src: getImage(img, scaleX, scaleY),
                   rotation: rotation
                })
            });
            return styles.concat([treeStyle2]);
    
        } else if (feature.getGeometry().getCenter) {
            treeStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
            // get rotation from drawn feature or geometry
            treeStyle.getImage().setRotation(feature.getGeometry().get('rotation'));
            treeStyle.getImage().setScale(feature.getGeometry().getRadius()/resAdjust);
            return treeStyle;
        } else {
           return styles;
        } 
    }
    
    var raster = new ol.layer.Tile({
        source: new ol.source.OSM()
    });
    
    var features = new ol.Collection();
    
    var source = new ol.source.Vector({wrapX: false, features: features});
    
    var vector = new ol.layer.Vector({
        source: source,
        style: styleFunction
    });
    
    var map = new ol.Map({
        layers: [raster, vector],
        target: 'map',
        view: new ol.View({
            center: [-11000000, 4600000],
            zoom: 4
        })
    });
    
    var draw = new ol.interaction.Draw({
        source: source,
        type: 'Circle',
        geometryFunction: function(coordinates, geometry) {
            var center = coordinates[0];
            var last = coordinates[1];
            var dx = center[0] - last[0];
            var dy = center[1] - last[1];
            var radius = Math.sqrt(dx * dx + dy * dy);
            var rotation = Math.PI - Math.atan2(dy, dx);
            geometry = geometry || new ol.geom.Circle(center, radius);
            geometry.setCenterAndRadius(center, radius);
            geometry.set('rotation', rotation);
            return geometry;
        },
        style: styleFunction
    });
    
    draw.on('drawstart', function () {
        //source.clear();
    });
    
    draw.on('drawend', function (evt) {
        // move rotation from geometry to drawn feature
        var rotation = evt.feature.getGeometry().get('rotation');
        evt.feature.set('rotation', rotation);
        var geom = ol.geom.Polygon.fromExtent(evt.feature.getGeometry().getExtent());
        geom.rotate(-rotation, evt.feature.getGeometry().getCenter());
        evt.feature.setGeometry(geom);
    });
    
    map.addInteraction(draw);
    
    var modify = new ol.interaction.Transform({
        features: features,
        translateFeature: false,
        // flip wouldn't be compatible with rotation
        noFlip: true 
    });
    
    var startangle = 0;
    
    modify.on('rotatestart', function(e) {
        startangle = e.feature.get('rotation') || 0;
    });
    
    modify.on('rotating', function (e) {
        // Set angle attribute to be used on style !
        e.feature.set('rotation', startangle - e.angle);
    });
    
    modify.on('select', function(e) {
        draw.setActive(e.features.length == 0);
    });
    
    map.addInteraction(modify);
    <link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
    <script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
    <script src="https://viglino.github.io/ol-ext/dist/ol-ext.js"></script>
    <div id="map" class="map"></div>

    Performance is improved by maintaining a cache of styled scaled icons, and the Transform interaction fits the icons better if the icons are aligned to a polygon which fits inside the original circle.

    var debug = true;  // show bounding polygons
    
    var img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = 'https://www.mikenunn.net/data/oak-tree-icon-png-17.png';
    
    var styleCache = {};
    
    function getStyle(img, scaleX, scaleY) {
        var x = Math.round(img.width * scaleX);
        var y = Math.round(img.height * scaleY);
        var key = img.src + ',' + x + ',' + y
        var style = styleCache[key]
        if (!style) {
            var canvas = document.createElement('canvas');
            canvas.width = x;
            canvas.height = y;
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, x, y);
            var url = canvas.toDataURL();
            canvas.remove();
            var keys = Object.keys(styleCache);
            if (keys.length >= 100) {
                // delete an old entry to limit the cache size
                delete styleCache[keys[0]];
            }
            var style = new ol.style.Style({
                image: new ol.style.Icon({
                   src: url
                })
            });
            styleCache[key] = style;
        }
        return style;
    }
    
    var white = [255, 255, 255, 1];
    var blue = [0, 153, 255, 1];
    var width = 3;
    styles = [
      new ol.style.Style({
       //     fill: new ol.style.Fill({
       //       color: [255, 255, 255, 0.5]
       //     })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: white,
          width: width + 2
        })
      }),
      new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: blue,
          width: width
        })
      }),
      new ol.style.Style({
        image: new ol.style.Circle({
          radius: width * 2,
          fill: new ol.style.Fill({
            color: blue
          }),
          stroke: new ol.style.Stroke({
            color: white,
            width: width / 2
          })
        }),
        zIndex: Infinity
      })
    ];
    
    var treeStyle = new ol.style.Style({
        image: new ol.style.Icon({
           src: img.src
        })
    });
    
    styleFunction = function(feature, resolution) {
        var resAdjust = 150 * resolution;
        var rotation = feature.get('rotation');
        if (rotation !== undefined) {
    
            var extent = feature.getGeometry().getExtent();
            var coordinates = feature.getGeometry().getCoordinates()[0];
    
            var left = coordinates[0];
            var bottom = coordinates[1];
            var right = coordinates[2];
            var top = coordinates[3];
            var center = ol.extent.getCenter(extent);
    
            var closest = new ol.geom.LineString([top, bottom]).getClosestPoint(left);
            var dx = closest[0] - left[0];
            var dy = closest[1] - left[1];
            var scaleX = Math.sqrt(dx * dx + dy * dy)/resAdjust;
    
            var dx = top[0] - center[0];
            var dy = top[1] - center[1];
            var scaleY = Math.sqrt(dx * dx + dy * dy)/resAdjust;
            var rotation = Math.atan2(dx, dy) - Math.PI;
    
            var treeStyle2 = getStyle(img, scaleX, scaleY);
            treeStyle2.setGeometry(new ol.geom.Point(center));
            treeStyle2.getImage().setRotation(rotation);
            return debug ? styles.concat([treeStyle2]) : treeStyle2;
    
        } else if (feature.getGeometry().getCenter) {
    
            treeStyle.setGeometry(new ol.geom.Point(feature.getGeometry().getCenter()));
            // get rotation from drawn feature or geometry
            treeStyle.getImage().setRotation(feature.getGeometry().get('rotation'));
            treeStyle.getImage().setScale(feature.getGeometry().getRadius()/resAdjust);
            return treeStyle;
    
        } else {
           return styles;
        } 
    }
    
    var raster = new ol.layer.Tile({
        source: new ol.source.OSM()
    });
    
    var features = new ol.Collection();
    
    var source = new ol.source.Vector({wrapX: false, features: features});
    
    var vector = new ol.layer.Vector({
        source: source,
        style: styleFunction
    });
    
    var map = new ol.Map({
        layers: [raster, vector],
        target: 'map',
        view: new ol.View({
            center: [-11000000, 4600000],
            zoom: 4
        })
    });
    
    var draw = new ol.interaction.Draw({
        source: source,
        type: 'Circle',
        geometryFunction: function(coordinates, geometry) {
            var center = coordinates[0];
            var last = coordinates[1];
            var dx = center[0] - last[0];
            var dy = center[1] - last[1];
            var radius = Math.sqrt(dx * dx + dy * dy);
            var rotation = Math.PI - Math.atan2(dy, dx);
            geometry = geometry || new ol.geom.Circle(center, radius);
            geometry.setCenterAndRadius(center, radius);
            geometry.set('rotation', rotation);
            return geometry;
        },
        style: styleFunction
    });
    
    draw.on('drawstart', function () {
        //source.clear();
    });
    
    draw.on('drawend', function (evt) {
        // move rotation from geometry to drawn feature
        var rotation = evt.feature.getGeometry().get('rotation');
        evt.feature.set('rotation', rotation);
        var geom = ol.geom.Polygon.fromCircle(evt.feature.getGeometry(), 4, -rotation);
        evt.feature.setGeometry(geom);
    });
    
    map.addInteraction(draw);
    
    var modify = new ol.interaction.Transform({
        features: features,
        translateFeature: false,
        // flip wouldn't be compatible with rotation
        noFlip: true 
    });
    
    var startangle = 0;
    
    modify.on('rotatestart', function(e) {
        startangle = e.feature.get('rotation') || 0;
    });
    
    modify.on('rotating', function (e) {
        // Set angle attribute to be used on style !
        e.feature.set('rotation', startangle - e.angle);
    });
    
    modify.on('select', function(e) {
        draw.setActive(e.features.length == 0);
    });
    
    map.addInteraction(modify);
    <link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
    <script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
    <script src="https://viglino.github.io/ol-ext/dist/ol-ext.js"></script>
    <div id="map" class="map"></div>