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

chore(web): add multi feature selection APIs #716

Merged
merged 5 commits into from
Oct 6, 2023

Conversation

keiya01
Copy link
Member

@keiya01 keiya01 commented Oct 2, 2023

Overview

I added some plugin API for multi feature selection.

  • selectFeatures ... Select multiple features
  • pickManyFromViewport ... Pick multiple features from rectangle on viewport
  • selectedFeatureColor appearance for 3dtiles(This will be added for other layer)

What I've done

  • Support multi feature selection for plugin API

What I haven't done

  • Support multi feature selection for ReEarth. It's still support only single selection.

How I tested

  1. Add layers
reearth.layers.add({
  type: "simple",
  data: {
      type: "3dtiles",
      url: "https://assets.cms.plateau.reearth.io/assets/4a/30f295-cd07-46b0-b0ab-4a4b1b3af06b/13100_tokyo23-ku_2022_3dtiles_1_1_op_bldg_13102_chuo-ku_lod2_no_texture/tileset.json"
  },
  ["3dtiles"]: {
      selectedFeatureColor: "blue"
  }
})

const layerId = reearth.layers.add({
  type: "simple",
  data: {
      type: "3dtiles",
      url: "https://assets.cms.plateau.reearth.io/assets/ca/ee4cb0-9ce4-4f6c-bca1-9c7623e84cb1/13100_tokyo23-ku_2022_3dtiles_1_1_op_bldg_13101_chiyoda-ku_lod2_no_texture/tileset.json"
  },
  ["3dtiles"]: {
    selectedFeatureColor: "red"
  }
})
setTimeout(() => reearth.camera.flyTo(layerId), 10)
  1. Select multiple features with this script(This script selects features within a rectangle at the center, so you need to move the camera to 3dtiles be the center of the camera). Then feature's color will be changed to selectedFeatureColor.
const x = window.innerWidth / 2;
const y = window.innerHeight / 2;
const features = reearth?.scene?.pickManyFromViewport(
  [x, y],
  100,
  100,
  (f) => "3dtiles" in f,
);
reearth.layers.selectFeatures(features.map(f => ({ layerId: f.layerId, featureId: [f.id] }))); 
  1. Select single feature with click. Then previous feature is unselected, and feature's color will be changed to selectedFeatureColor.

Which point I want you to review particularly

Memo

@keiya01 keiya01 requested review from airslice and pyshx October 2, 2023 01:38
@netlify
Copy link

netlify bot commented Oct 2, 2023

Deploy Preview for reearth-web ready!

Name Link
🔨 Latest commit fb6fcf3
🔍 Latest deploy log https://app.netlify.com/sites/reearth-web/deploys/651fb02868c33b00081c8f99
😎 Deploy Preview https://deploy-preview-716--reearth-web.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@codecov
Copy link

codecov bot commented Oct 2, 2023

Codecov Report

Merging #716 (3ab8ef1) into main (af62fb4) will increase coverage by 0.00%.
The diff coverage is 33.73%.

❗ Current head 3ab8ef1 differs from pull request most recent head fb6fcf3. Consider uploading reports for the commit fb6fcf3 to get more accurate results

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main     #716    +/-   ##
========================================
  Coverage   26.73%   26.74%            
========================================
  Files        1571     1574     +3     
  Lines      171278   172027   +749     
  Branches     3896     3913    +17     
========================================
+ Hits        45794    46004   +210     
- Misses     124395   124934   +539     
  Partials     1089     1089            
Flag Coverage Δ
web 25.06% <33.73%> (+0.03%) ⬆️
web-beta 25.06% <33.73%> (+0.03%) ⬆️
web-classic 25.06% <33.73%> (+0.03%) ⬆️
web-utils 25.06% <33.73%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
web/src/beta/lib/core/Map/Layers/index.tsx 100.00% <100.00%> (ø)
web/src/beta/lib/core/Map/Layers/keys.ts 100.00% <100.00%> (ø)
web/src/beta/lib/core/Map/ref.ts 89.62% <100.00%> (+0.62%) ⬆️
...src/beta/lib/core/engines/Cesium/Feature/utils.tsx 67.74% <100.00%> (+0.26%) ⬆️
web/src/beta/lib/core/mantle/types/appearance.ts 100.00% <100.00%> (ø)
web/src/beta/lib/core/mantle/types/index.ts 100.00% <100.00%> (ø)
web/src/beta/lib/core/utils/assertType.ts 100.00% <100.00%> (ø)
web/src/beta/lib/core/utils/index.ts 100.00% <100.00%> (ø)
web/src/beta/lib/core/Map/index.tsx 0.00% <0.00%> (ø)
web/src/beta/lib/core/Crust/Plugins/storybook.tsx 0.00% <0.00%> (ø)
... and 13 more

... and 17 files with indirect coverage changes

Copy link
Contributor

@pyshx pyshx left a comment

Choose a reason for hiding this comment

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

Thanks for you hard work, I've added a few refactors. Rest is looking good to me.

@@ -747,8 +777,74 @@ function useSelection({
[],
);

const selectedFeatureIds = useRef<{ layerId: string; featureIds: string[] }[]>([]);
const selectFeatures = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you divide the selectFeatures function into smaller functions?

);
else if (options) setSelectedLayer(s => [s[0], options, info]);
else
setSelectedLayer(s => (!s[0] && !s[1] && !s[2] ? s : [undefined, undefined, undefined]));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
setSelectedLayer(s => (!s[0] && !s[1] && !s[2] ? s : [undefined, undefined, undefined]));
const resetSelectedLayer = (s) => (!s[0] && !s[1] && !s[2] ? s : [undefined, undefined, undefined]);
// Usage
setSelectedLayer(resetSelectedLayer);

colorScratch.green = Color.byteToFloat(pixels[index + 1]);
colorScratch.blue = Color.byteToFloat(pixels[index + 2]);
colorScratch.alpha = Color.byteToFloat(pixels[index + 3]);
const object = context.getObjectByPickColor(colorScratch);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we wrap this in try and catch..?

return offCenter.computeCullingVolume(camera.positionWC, camera.directionWC, camera.upWC);
}

function getIntersection(
Copy link
Contributor

@pyshx pyshx Oct 4, 2023

Choose a reason for hiding this comment

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

Its better to minimize side effects in functional programming. here It's modifying result directly. Instead, consider returning a new object:

function getIntersection(
a: BoundingRectangle,
b: BoundingRectangle,
result = new BoundingRectangle(),
): BoundingRectangle | undefined {
//...
return {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
};

Just as a practice if you can apply similar thing to other functions that'll be great 🙏

const width = screenSpaceRectangle.width ?? 1;
const height = screenSpaceRectangle.height ?? 1;
const context = this._context;
const pixels = context.readPixels({
Copy link
Contributor

Choose a reason for hiding this comment

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

Now this one is optional,
You might want to add validations for the returned pixels and handle potential issues gracefully, to avoid unexpected behavior in subsequent lines of code.

windowHeight: number,
// TODO: Get condition as expression for plugin
condition?: (f: PickedFeature) => boolean,
) => PickedFeature[] | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

consider returning an empty array as a falsy value instead of undefined

const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
findFeaturesFromLayer(viewer, layerId, featureId, entity => {
const tag = getTag(entity) ?? {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const tag = getTag(entity) ?? {};
const tag = getTag(entity) ?? {};
const updatedTag = { ...tag, isFeatureSelected: true };

Lets avoid direct manipulation

Comment on lines 559 to 561
return findFeaturesFromLayer(viewer, layerId, featureIds, e => {
return convertObjToComputedFeature(viewer, e)?.[1];
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return findFeaturesFromLayer(viewer, layerId, featureIds, e => {
return convertObjToComputedFeature(viewer, e)?.[1];
});
return findFeaturesFromLayer(viewer, layerId, featureIds, e => convertObjToComputedFeature(viewer, e)?.[1]);

Since you’re using arrow functions and implicit returns, consider adopting a more functional style for simple transformations. I might be worth doing the same for the rest of places where this practice can be adopted.

layerId: string,
featureId: string[],
convert?: (e: Entity | Cesium3DTileset | InternalCesium3DTileFeature) => T,
): NonNullable<T>[] | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of returning undefined, consider returning an empty array to keep the return
Same as above.[OPTIONAL]

@@ -151,6 +189,63 @@ export function findEntity(
return;
}

export function findFeaturesFromLayer<T = Entity | Cesium3DTileset | InternalCesium3DTileFeature>(
Copy link
Contributor

Choose a reason for hiding this comment

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

If possible, It'd be great if you can breakdown this function into smaller[OPTIONAL].

function filterEntitiesById(entities: Entity[], layerId: string, featureId: string[]): Entity[] {
// filtering logic
}

function getEntitiesFromDatasources(viewer: CesiumViewer, layerId: string, featureId: string[]): Entity[] {
// logic to get entities from datasources
}

function get3DTileFeatures(viewer: CesiumViewer, featureId: string[]): Set<Entity | Cesium3DTileset | InternalCesium3DTileFeature> {
// logic to get entities from 3D Tiles
}

@keiya01 keiya01 merged commit 124d3c1 into main Oct 6, 2023
7 of 8 checks passed
@keiya01 keiya01 deleted the feat/multi-feature-selection branch October 6, 2023 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants