Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select control #897

Merged
merged 39 commits into from Aug 30, 2013
Merged

Select control #897

merged 39 commits into from Aug 30, 2013

Conversation

ahocevar
Copy link
Member

This pull request implements the Select control as described in https://github.com/openlayers/ol3/wiki/Vector-Editing#olcontrolselect. Button css is added in the example only for now, in anticipation with the ongoing control button refactoring that should simplify button css handling.

@tschaub
Copy link
Member

tschaub commented Aug 15, 2013

Cool to see this in action @ahocevar. We've discussed this, but just to be clear here, this doesn't do anything with features on the existing layer (it doesn't remove them or suggest to the renderer that they be hidden). It just adds the same features to a new layer.

I still think it makes sense to clone the features before adding them to a new layer. The idea behind the render intent would be that it would be a styling hook. So we'd tell the original layer that the rendering intent for the selected features is "hidden" (or similar). And then after cloning the layer, we'd set the render intent to "select" and add them to the temp layer. This would allow the user to decide how selected features should be styled with filter syntax like this:

new ol.style.Style({rules: [
  new ol.style.Rule({
    filter: 'this.renderIntent == "selected"'
    symbolizers: [ /** symbolizers for selected style here */ ]
  })
]});

I can also imagine we might want to have one temp layer per original layer. This would make it easier to handle applications that let people do things like remove a layer or choose new layers for editing during an editing session. It would also allow the control to clone the original layer's style for the temp layer. This would mean people could just configure their styles in one place and add rules for styling selected or temporary features.

The feature cloning and one temp per original layer can change later - we just want to make sure they are considered before adding a way for people to style their selected features.

I'll take a closer look tomorrow, but I'm looking forward to having this functionality in.

@elemoine
Copy link
Member

And then after cloning the layer,

Tim, did you mean "after cloning the feature"?

@elemoine
Copy link
Member

Don't we want that the feature selection/deselection logic be implemented in an interaction rather in the control? With this the select control would just add the button, and add/remove the interaction when the button is clicked. This can probably be changed later, but I just wanted to know if it makes sense to people.

@@ -337,6 +346,7 @@
* @property {number|undefined} opacity Opacity. 0-1. Default is 1.
* @property {ol.source.Source} source Source for this layer.
* @property {ol.style.Style|undefined} style Style.
* @property {boolean|undefined} temp True for temporary layers. For internal use only.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to export that property if it's for internal use only? How about adding a private setTemporary method to ol.layer.Vector instead?

@ahocevar
Copy link
Member Author

@elemoine If we wanted to split this into control and interaction, the control would have to be responsible for adding the interaction to the map, and would have to activate and deactivate the interaction. Is this what you're envisioning? I also think this can be changed later if we want to have this split.

@ahocevar
Copy link
Member Author

@tschaub Thanks for your great suggestions. I see a way now how the mapping between features on the source layer and the target sketch layer can work efficiently. I'll add a commit. I also like the idea of having a separate sketch layer for each source layer and cloning styles of the original layer. I'll add a commit for that as well.

@elemoine
Copy link
Member

Yes, this is what I envisioned. Adding a select control/toggle button should not be necessary to activate interactive feature selection. Also programmatically selecting features should be possible, eventually.

@tschaub
Copy link
Member

tschaub commented Aug 17, 2013

As it is, you can activate the control programmatically. The "interaction" is just a click. What else were you envisioning that the interaction would do?

@elemoine
Copy link
Member

ol2's select control doesn't add any DOM element. It just enables feature selection on click or drag-box. So I tend to think that his functionality should be implemented in an interaction in ol3 (for example, ol.interaction.ClickSelect and ol.interaction.DragSelect). The ol.control.Select control would add/remove the interaction when the toggle button is pressed/depressed. I haven't thought too much about it, so please tell me if that doesn't make sense to you.

@ahocevar ahocevar mentioned this pull request Aug 20, 2013
@ahocevar
Copy link
Member Author

@tschaub I implemented your suggestions, and also the renderIntent handling. I think this is in good shape now. You may want to continue/finish your review. Thanks!

@ahocevar
Copy link
Member Author

@elemoine I spent a bit more time thinking about the control - interaction split. As far as I can see, interactions are designed to change the state of the map's view. The select control does not do that. It manages its own state by keeping track of selected/unselected features, and manages temporary layers to display them. A BoxSelect interaction would have to share the state with a ClickSelect interaction, which suggests that a third component is needed for the state and the selection layer management. And a fourth for providing the button if people want to use it as control. All this makes me think that for now we should stick with the control, and if we come up with something smart we can change it later. Having the events dispatched on the control is not a problem for now, because even if we decide to change that later, we can still relay the events on the control.

@elemoine
Copy link
Member

@elemoine I spent a bit more time thinking about the control - interaction split. As far as I can see, interactions are designed to change the state of the map's view.

To me interactions just receive map browser events. They may perform any action. The interactions we currently have in master update the view, but I think we could have interactions that call map.getFeatures.

Also, interactions are organized in a prioritized array. This means that a "select interaction" could steal the map browser event, and prevent lower-priority interactions from getting that event.

A BoxSelect interaction would have to share the state with a ClickSelect interaction, which suggests that a third component is needed for the state

In ol2 the arrays of selected features are on the layers.

This makes me wonder: with the current ol.control.Select code, what do we do when a vector layer is removed? Should the control listen to "remove layer" events and clean up its selected features array?

All this makes me think that for now we should stick with the control

Yes, the control is a high-level component. If we agree on, and restrict, the control API we can later decompose the code into separate modules.

@ahocevar
Copy link
Member Author

Thanks for your valuable comment @elemoine. You are right - managing addition and removal of layers is still a TODO. If we decide to handle feature selection on the layer, it needs to be done on ol.layer.Layer, not on ol.layer.Vector - because you can select features on all layers whose sources can provide access to the underlying features (e.g. WMS through WFS or GML-GetFeatureInfo).

@elemoine
Copy link
Member

Some comments on the control API:

Currently an array of layers can be passed to the ol.control.Select constructor. The control keeps a reference to that array. As layers are added to/removed from the map, and made visible/invisible, the control and/or the control user need to update that array. The bookkeeping of this array can be tedious and error-prone.

What about working with a layer filter function instead? That function would take a layer as its argument and return a boolean value. For example, if you don't want invisible layers to participate in the layer selection you would do:

var control = new ol.control.Select({
  filter: function(layer) {
    return layer.getVisible();
  }
});

And with this, layers that are not in the map would automatically be excluded from the layer selection.

Thoughts?

@schedul-xor
Copy link

Your work is very cool, @ahocevar !

I tried this at my environment, and would like to add some comments.

  • Since the "selection" action is detected only when there is no dragging event between mousedown and mouseup, I fail to select once in three times when I click on a polygon (maybe I'm dragging few pixels from mousedown to mouseup). I suggest to add an "dragging disabled mode on/off" control to map, and discard mousemove events while dragging is disabled.
  • If you're looking forward to implementing "Point selection" (actually what I want it), nearest neighbor point searching function in R-Tree structure will be required (It is nearly impossible to let users click one single pixel). I've implemented the nearest neighbor point searching function in my own branch https://github.com/schedul-xor/ol3/tree/rtree . Currently, R-Tree structure is used only for vector feature caching, and it's property is private, so it is necessary to make it a public property.

@ahocevar
Copy link
Member Author

@schedul-xor Thanks for your feedback.

Point selection is already implemented, and it is even smarter than nearest neighbor search, because it takes the actual symbolizer into account. And it also uses the R-Tree. You can see this outside the context of the Select control in http://ol3js.org/en/r3.0.0-alpha.4/examples/gpx.html - just hover over the points.

Regarding the click selection, there is a trade-off between a pure selection mode where map dragging is disabled and the convenience of being able to drag the map. But you can always remove the dragpaninteraction from the map when you want pure selection mode.

@elemoine
Copy link
Member

@elemoine
Copy link
Member

Strange user facing API aside

Passing filter functions to functions/constructors is a typical JavaScript pattern.

you would still have to do the same bookkeeping, even with a filter function. Because you need to create/remove/show/hide the selection layers. Or am I missing something?

Couldn't we lazily create selection layers? A feature is clicked, if we don't have a selection layer with the proper style for that feature then we create a new selection layer, otherwise we reuse the selection layer. In this way, we don't have a 1:1 mapping between original layers and selection layers, which means no bookkeeping.

Now, what to do when we have selected features and the original layer gets removed from the map, or is made invisible, is application-specific. There are certainly use-cases where you want the selection to remain visible, and others where you don't.

Our controls are currently not designed to be removed from a map. Once we have an ol.control.Control#removeMap() method, we can add clean up code. But without such an API in place, I think that is beyond the scope of this ticket.

We have ol.Map#removeControl, which does control.setMap(null). I think your code re-registers a listener when removeControl is called.

@ahocevar
Copy link
Member Author

Thanks @elemoine, I'll give it a try. There will still be some bookkeeping to see if we have a selection layer already.

@tschaub
Copy link
Member

tschaub commented Aug 22, 2013

I like the idea of filter functions, but they are more of a hassle if you have multiple layers without clearly distinguishable properties. Say you have four vector layers and want one editable/selectable, it means you have to give the layer some id and then create a function that returns true if the id matches. It is more straightforward to pass a reference to the layer itself.

A flexible API would take a filter function or a list of layers.

@ahocevar
Copy link
Member Author

@elemoine I have converted the control to an interaction. Would be great if you could take a look. Thanks.

@elemoine
Copy link
Member

Thanks @ahocevar. Will have a look this afternoon.

<div id="docs">
<p>See the <a href="select-features.js" target="_blank">select-features.js source</a> to see how this is done.</p>
</div>
<div id="tags">SelectFeature, vector</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"select" sounds like a better tag than "SelectFeature" to me. This is minor.

@elemoine
Copy link
Member

The select-features example does not work in advanced mode (select-features.html?mode=advanced). Is it because the feature.renderIntent property gets renamed, preventing the rule filter from doing its job?

ol.Feature.prototype.clone = function() {
var clone = new ol.Feature(this.getAttributes());
clone.setGeometry(this.getGeometry().clone());
clone.featureId_ = this.featureId_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ol.feature#getFeatureId API doc says: "This identifier is usually the unique id in the source store", so I'm wondering if the clone should have the same featureId as the original feature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source store is the server. Think of it as GML fid. If the use case is to clone a feature because it will be persisted on the server with changes later, then the current behavior makes sense. If the use case is to create a copy and duplicate the feature on the server too, then it would be better to not use the same fid for the clone. So how should we proceed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this suggests that the decision should be made at the application level.

What we need here is a mix of deep and shallow cloning, and we
do not want to do this in a generic ol.Feature#clone() method.
renderer: ol.RendererHint.CANVAS,
target: 'map',
view: new ol.View2D({
center: [-10997171.194994785, 5206335.565590534],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (and I know this is a problem in the example from which it was copied as well), but it would be nice to center the view on the visible data. Something like [-11000000, 4600000] makes a nice starting point.

@tschaub
Copy link
Member

tschaub commented Aug 29, 2013

Really nice work @ahocevar. I'm in favor of merging this in - the comments I added above can be addressed now or later.

I'll admit I'm not totally sold on "select" as an interaction (wondering if instead a more basic "feature" interaction makes sense). I do think we should get this in and move on to editing to see how the different parts work best together.

ahocevar added a commit that referenced this pull request Aug 30, 2013
@ahocevar ahocevar merged commit 29317c3 into openlayers:master Aug 30, 2013
@ahocevar ahocevar deleted the select branch August 30, 2013 13:25
@elemoine
Copy link
Member

I was in the process of reviewing :) No problem. I'll make comments anyway, if I have some.

@ahocevar
Copy link
Member Author

Sorry for that @elemoine - additional comments welcome of course.

};
goog.inherits(ol.interaction.Interaction, goog.Disposable);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the map calls ol.interaction.defaults() to create the default collection of interactions, it should also call registerDisposable on each interaction of the collection.

With the map not disposing of the interactions it creates, and with no exports of our disposable objects' dispose methods, it is useless to have ol.interaction.Interaction inherit from goog.Disposable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elemoine So effectively this means I can also remove ol.interaction.Select#disposeInternal()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.

And in any way isn't it when the interaction is removed from the map that the selection layers should be removed, etc.?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I don't see a way to do that because there is no hook that is called when an interaction is removed from the map. For now I'm adding some code to remove selection layers when they do not contain any selection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until the select interaction the interactions were all stateless. We need to think this through I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @tschaub said, it may not be the best thing to have such a high level interaction. But let's see how we get along with it when we work on editing, and make changes as we need them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@elemoine
Copy link
Member

I added a few comments. And I'm done.

ahocevar added a commit to ahocevar/openlayers that referenced this pull request Aug 30, 2013
* Interaction is no goog.Disposable any more.
* Permanent cleanup during selection instead of disposeInternal.
* Moved selectionLayers creation outside feature loop.
* Maintain selectedFeatures and unselectedFeatures only for
  layers that have a setRenderIntent method.
@ahocevar
Copy link
Member Author

@elemoine Please take a quick look at #945, which addresses most of your post-merge comments. The only one I did not address was the goog.isFunction() check for getStyle(), because we do similar duck typing for setRenderIntent(). We do this elsewhere too, and to make it less evil we could define an interface that the vector layer and other potential layers that can display and style features implement.

ahocevar added a commit that referenced this pull request Sep 2, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants