So I'm using https://github.com/alex3165/react-leaflet-draw
I want to make some particular polygon editable when you click a pencil button outside of the map. Right now there is a button "edit layers" but it naturally makes all of them editable, can I do it on polygon by polygon basis?
So I would want to do smth like this:
map to edit polygons
I opened an issue https://github.com/alex3165/react-leaflet-draw/issues/79 but not much success there
The thing is through refs I can get a giant circular object: this is what I get when I console.log .drawControl._toolbars.edit._modes.edit.handler.options.featureGroup._layers[57]
One option to consider is to create editable layer per every shape(feature), in a way that instead of creating EditControl control for a collection of features:
<FeatureGroup>
<EditControl
position="topright"
/>
</FeatureGroup>
create EditControl instance per every feature from GeoJSON:
<FeatureGroup>
<EditControl
position="topright"
/>
</FeatureGroup>
...
<FeatureGroup>
<EditControl
position="topright"
/>
</FeatureGroup>
The following example demonstrates this approach where:
only a single shape could be edited at a time
switching from one editable layer to another occurs on layer click
Example:
function EditableGroup(props) {
const [selectedLayerIndex, setSelectedLayerIndex] = useState(0);
function handleLayerClick(e) {
setSelectedLayerIndex(e.target.feature.properties.editLayerId);
}
let dataLayer = new L.GeoJSON(props.data);
let layers = [];
let i = 0;
dataLayer.eachLayer((layer) => {
layer.feature.properties.editLayerId = i;
layers.push(layer);
i++;
});
return (
<div>
{layers.map((layer, i) => {
return (
<EditableLayer
key={i}
layer={layer}
showDrawControl={i === selectedLayerIndex}
onLayerClicked={handleLayerClick}
/>
);
})}
</div>
);
}
where
function EditableLayer(props) {
const leaflet = useLeaflet();
const editLayerRef = React.useRef();
let drawControlRef = React.useRef();
let {map} = leaflet;
useEffect(() => {
if (!props.showDrawControl) {
map.removeControl(drawControlRef.current);
} else {
map.addControl(drawControlRef.current);
}
editLayerRef.current.leafletElement.clearLayers();
editLayerRef.current.leafletElement.addLayer(props.layer);
props.layer.on("click", function (e) {
props.onLayerClicked(e, drawControlRef.current);
});
}, [props, map]);
function onMounted(ctl) {
drawControlRef.current = ctl;
}
return (
<div>
<FeatureGroup ref={editLayerRef}>
<EditControl
position="topright"
onMounted={onMounted}
{...props}
/>
</FeatureGroup>
</div>
);
}
Here is a demo for the reference
const [mapLayers, setMapLayers] = useState([]);
const _onEdited = (e) => {
e.layers.eachLayer(a => {
const { _leaflet_id } = a;
setMapLayers((layers) => [
...layers,
{ id: _leaflet_id, latlngs: a.getLatLngs()[0] },
]);
});
}
Related
Following the Mapbox draw example I can use the draw variable to access all features that are drawn on a map.
const draw = new MapboxDraw({
// ...
});
map.addControl(draw);
// ...
function updateArea(e) {
const data = draw.getAll(); // Accessing all features (data) drawn here
// ...
}
However, in react-map-gl library useControl example I can not figure out how to pass ref to the DrawControl component so I can use it as something like draw.current in a similar way as I did draw in normal javascript above.
In my DrawControl.jsx
const DrawControl = (props) => {
useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
In my MapDrawer.jsx
import Map from 'react-map-gl';
import DrawControl from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = React.useRef(null);
const onUpdate = React.useCallback((e) => {
const data = draw.current.getAll(); // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
ref={draw}
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
I also get an error stating I should use forwardRef but I'm not really sure how.
react_devtools_backend.js:3973 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
What I need is basically to delete the previous feature if there is a new polygon drawn on a map so that only one polygon is allowed on a map. I want to be able to do something like this in the onUpdate callback.
const onUpdate = React.useCallback((e) => {
// ...
draw.current.delete(draw.current.getAll.features[0].id);
// ...
}, []);
I had the similar problem recently with that lib, I solved it doing the following :
export let drawRef = null;
export default const DrawControl = (props) => {
drawRef = useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
import DrawControl, {drawRef} from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = drawRef;
const onUpdate = React.useCallback((e) => {
const data = draw?draw.current.getAll():null; // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
const onUpdate = React.useCallback((e) => {
// ...
drawRef.delete(drawRef.getAll.features[0].id);
// ...
}, []);
Once component created, the ref is available for use.
Not that elegant but working... Sure there might be cleaner way...
Hope that helps!
Cheers
Pass draw from draw control to parent component.
const DrawControl = (props) => {
const [draw, setDraw] = useState()
const { setDraw: setDrawInParent, onUpdate, onCreate, onDelete } = props;
useEffect(() => {
if (draw) setDrawInParent(draw)
}, [draw])
useControl(
({ map }) => {
map.on("draw.create", onCreate);
map.on("draw.update", onUpdate);
map.on("draw.delete", onDelete);
const draw = new MapboxDraw(props);
setDraw(draw);
return draw;
}
);
return null;
};
I think I found a better solution combine forwardRef and useImperativeHandle to solve:
export const DrawControl = React.forwardRef((props: DrawControlProps, ref) => {
const drawRef = useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ map }) => {
map.on("draw.create", props.onCreate);
map.on("draw.update", props.onUpdate);
map.on("draw.delete", props.onDelete);
map.on("draw.modechange", props.onModeChange);
},
({ map }) => {
map.off("draw.create", props.onCreate);
map.off("draw.update", props.onUpdate);
map.off("draw.delete", props.onDelete);
map.off("draw.modechange", props.onModeChange);
},
{
position: props.position,
}
);
React.useImperativeHandle(ref, () => drawRef, [drawRef]); // This way I exposed drawRef outside the component
return null;
});
in the component:
const drawRef = React.useRef<MapboxDraw>();
const [drawMode, setDrawMode] = React.useState<DrawMode>(“draw_polygon");
const changeModeTo = (mode: DrawMode) => {
// If you programmatically invoke a function in the Draw API, any event that directly corresponds with that function will not be fired
drawRef.current?.changeMode(mode as string);
setDrawMode(mode);
};
<>
<DrawControl
ref={drawRef}
position="top-right”
displayControlsDefault={false}
controls={{
polygon: true,
trash: true,
}}
defaultMode=“draw_polygon"
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onModeChange={onModeChange}
/>
<button
style={{
position: ‘absolute’,
left: ‘20px’,
top: ‘20px’,
backgroundColor: '#ff0000’,
}}
onClick={() => changeModeTo('simple_select’)}
>
Change to Simple Select
</button>
<>
I have developed react-native cli, react-native-video (Currently, tried to expo-video).
In a screen, there are one flatList, it has a renderItem(column2). In a renderItem renders image or video thumbnail. Like shopping app UI AMAZON.
The problem is videos. It will stops automatically by user's swipe down to phone's view. Simultaneously, below stopped video, other video player will stops. This video is in exactly middle of user's phone view!
How can i solve this?
FlatList,
const [videoPlayback, setVideoPlayback] = useState(false);
const viewabilityConfig = {
itemVisiblePercentThreshold: 80,
};
const _onViewableItemsChanged = useCallback((props) => {
const changed = props.changed;
changed.forEach((item) => {
if (item.item.vimeoId) {
if (item.isViewable) {
setVideoPlayback((prev) => true);
} else {
setVideoPlayback((prev) => false);
}
}
});
}, []);
...
<FlatList
ref={ref}
numColumns={2}
data={posts}
keyExtractor={(item: any) => `${item.postId}`}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={_onViewableItemsChanged}
renderItem={({ item, index }) => (
<PostContainer item={item} videoPlayback={videoPlayback} navigation={navigation} index={index} />
)}
PostContainer
const PostContainer = ({ item, videoPlayback }) => {
const videoPlayer = useRef(null);
useFocusEffect(
React.useCallback(() => {
videoPlayer?.current?.seek(0);
if (videoPlayer && !videoPlayback) {
videoPlayer?.current?.seek(0);
}
}, [fadeAnim, videoPlayback]),
);
return (
<View>
{ // This place that image component rendered}
{item.vimeoId && (
<StyledVideo
ref={videoPlayer}
paused={!videoPlayback}
repeat={true}
resizeMode={"cover"}
volume={0}
source={{
uri: item.vimeoThumbnail,
}}
/>
)}
</View>
);
};
All the videos in your list share the same videoPlayback state. If you want each video to be controlled individually, move your videoPlayback state logic into your PostContainer.
I solved my problem. There are no fault in my logic. the important thing is 'What videos are current playing?'
FlatList
const _onViewableItemsChanged = useCallback(
(props) => {
const changed = props.changed;
changed.forEach((item) => {
if (item.item.vimeoId) {
const tempId = item.item.vimeoId;
if (item.isViewable) {
if (readVideosVar.list.indexOf(tempId) === -1) {
dispatch(postSlice.actions.addId(tempId));
}
} else {
dispatch(postSlice.actions.deleteId(tempId));
}
}
});
},
[dispatch],
);
redux
addId: (state, action) => {
if (state.list.indexOf(action.payload) !== -1) {
return;
} else {
let tempA = [];
tempA.push(action.payload);
state.list = state.list.concat(tempA);
}
},
deleteId: (state, action) => {
if (state.list.indexOf(action.payload) === -1) {
return;
} else {
let stand = state.list;
let findIdx = stand.indexOf(action.payload);
let leftA = stand.slice(0, findIdx);
let rightA = stand.slice(findIdx + 1);
state.list = leftA.concat(rightA);
}
},
Compared to the existing code, I had added 'redux-toolkit'. In dispatch logic, "onViewalbeItems" had tracked chagend viewable items by real-time.
And then, I also added,
export const viewabilityConfig = {
minimumViewTime: 1000,
viewAreaCoveragePercentThreshold: 0,
};
This is just sugar.
I solved it like this !
I am using #react-google-maps/api and I'm trying to set click event on gmap features which appends its area to React state.
enter image description here
but every time I click the feature, it seems the state has been set to empty array(initial state). and the event append one string to it. so the state doesn't get longer.
in this component i put one button that fires the same click event and that works fine. I don't know why this happens.
const MapComponent = ({area, setArea}) => {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: "APIKEY"
})
const onClickFeature = e => {
let tgt = e.feature.getProperty("S_NAME")
if(tgt){
addArea(tgt)
}
}
const addArea = tgt => {
let copy = [...area]
copy.push(tgt)
setArea(copy)
}
const renderMap = () => {
const onLoad = async map => {
await getGeoJson().then(res => {
initPolygon(map,res) //draw polygons with feature collection
map.data.addListener("click", e => {
onClickFeature(e)
})
})
}
return <><GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={14}
onLoad={onLoad}
onClick={onClickFeature}
>
</GoogleMap>
<button onClick={() => addArea('someplace')}>CLICK</button> {/* This works fine */}
<div>Selected: {area} </div>
</>}
if (loadError) {
return <div>Map cannot be loaded right now, sorry.</div>
}
return isLoaded ? renderMap() : null
}
I can see that your use case should have an area state which should be an array and everytime the geoJsondata feature is clicked, it will get the value of "S_NAME" property. Then it will add it to the area state array.
As you have not provided your whole code, I created your use case using the Google Maps sample geojson data. Everytime the feature is clicked, it will get the "title" property and add it to the "area" state.
Here's the sample code (Note: please use your API key in the index.js for the code to work) and code snippet below:
import React, { useState } from 'react';
import { GoogleMap } from '#react-google-maps/api';
const defaultLocation = { lat: -28, lng: 137 };
function Map() {
const [area, setArea] = useState([]);
const handleOnLoad = map => {
map.data.loadGeoJson(
'https://storage.googleapis.com/mapsdevsite/json/google.json'
);
map.data.addListener('click', onClickFeature);
};
const onClickFeature = event => {
console.log(event.feature.getProperty('letter'));
let letter = event.feature.getProperty('letter');
if (letter) {
addArea(letter);
}
};
const addArea = letter => {
let copy = area;
copy.push(letter);
setArea(copy);
//you will see here that the value of area array is being updated
console.log(area);
};
return (
<GoogleMap
center={defaultLocation}
zoom={5}
onLoad={handleOnLoad}
mapContainerStyle={{ width: '100%', height: '88vh' }}
/>
);
}
export default Map;
I wanted to create drawing app in React and to achieve that I came across something called Konvajs. Then, I started and somehow achieved that I can draw non-straight lines like this
But what I wanted to draw is straight lines like this
and rectangle. Here is my code:
import React, { Component } from "react";
import { Stage, Layer, Line, Rect, Text } from "react-konva";
export default class Canvas extends Component {
state = {
lines: [],
};
handleMouseDown = () => {
this._drawing = true;
this.setState({
lines: [...this.state.lines, []],
});
};
handleMouseUp = () => {
this._drawing = false;
};
handleMouseMove = (e) => {
if (!this._drawing) {
return;
}
const stage = this.stageRef.getStage();
const point = stage.getPointerPosition();
const { lines } = this.state;
let lastLine = lines[lines.length - 1];
lastLine = lastLine.concat([point.x, point.y]);
lines.splice(lines.length - 1, 1, lastLine);
this.setState({
lines: lines.concat(),
});
};
render() {
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onContentMousedown={this.handleMouseDown}
onContentMousemove={this.handleMouseMove}
onContentMouseup={this.handleMouseUp}
ref={(node) => {
this.stageRef = node;
}}
>
<Layer>
<Text text="Draw a thing!" />
{this.state.lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
}
}
For straight lines replace this
lastLine = lastLine.concat([point.x, point.y]);
With this
lastLine = [lastLine[0], lastLine[1], point.x, point.y]
Basically, you should have maximum four points. The first two are created OnMouseDown and the last two should be updated as you are moving the cursor - OnMouseMove
Here is the original example of group checkbox of antd that I need and its fine:
const plainOptions = ['Apple', 'Pear', 'Orange'];
const defaultCheckedList = ['Apple', 'Orange'];
class App extends React.Component {
state = {
checkedList: defaultCheckedList,
indeterminate: true,
checkAll: false,
};
onChange = checkedList => {
this.setState({
checkedList,
indeterminate: !!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length,
});
};
onCheckAllChange = e => {
this.setState({
checkedList: e.target.checked ? plainOptions : [],
indeterminate: false,
checkAll: e.target.checked,
});
};
render() {
return (
<div>
<div style={{ borderBottom: '1px solid #E9E9E9' }}>
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
Check all
</Checkbox>
</div>
<br />
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
);
}
}
My question is how can I replace the plainOptions and defaultCheckedList by object array instead of simple array and using attribute name for this check boxes?
For example this object:
const plainOptions = [
{name:'alex', id:1},
{name:'milo', id:2},
{name:'saimon', id:3}
];
const defaultCheckedList = [
{name:'alex', id:1},
{name:'milo', id:2}
];
I want to use attribute name as the key in this example.
Problem solved. I should use "Use with grid" type of group checkbox. It accepts object array. The only think I could do was creating a function that inject "label" and "value" to my object. It makes some duplicates but no problem.
function groupeCheckboxify(obj, labelFrom) {
for (var i = 0; i < obj.length; i++) {
if (obj[i][labelFrom]) {
obj[i]['label'] = obj[i][labelFrom];
obj[i]['value'] = obj[i][labelFrom];
}
if (i == obj.length - 1) {
return obj;
}
}
}
// for calling it:
groupeCheckboxify( myObject , 'name');
I'd this same problem and couldn't find any answer on the entire web. But I tried to find a good way to handle it manually.
You can use this code:
import { Checkbox, Dropdown } from 'antd';
const CheckboxGroup = Checkbox.Group;
function CheckboxSelect({
title,
items,
initSelectedItems,
hasCheckAllAction,
}) {
const [checkedList, setCheckedList] = useState(initSelectedItems || []);
const [indeterminate, setIndeterminate] = useState(true);
const [checkAll, setCheckAll] = useState(false);
const onCheckAllChange = (e) => {
setCheckedList(e.target.checked ? items : []);
setIndeterminate(false);
setCheckAll(e.target.checked);
};
const onChangeGroup = (list) => {
if (hasCheckAllAction) {
setIndeterminate(!!list.length && list.length < items.length);
setCheckAll(list.length === items.length);
}
};
const updateItems = (el) => {
let newList = [];
if (el.target.checked) {
newList = [...checkedList, el.target.value];
} else {
newList = checkedList.filter(
(listItem) => listItem.id !== el.target.value.id,
);
}
setCheckedList(newList);
};
useEffect(() => {
setCheckedList(initSelectedItems);
}, []);
const renderItems = () => {
return (
<div classname="items-wrapper">
{hasCheckAllAction ? (
<Checkbox
indeterminate={indeterminate}
onChange={onCheckAllChange}
checked={checkAll}
>
All
</Checkbox>
) : null}
<CheckboxGroup onChange={onChangeGroup} value={checkedList}>
<>
{items.map((item) => (
<Checkbox
key={item.id}
value={item}
onChange={($event) => updateItems($event)}
>
{item.name}
</Checkbox>
))}
</>
</CheckboxGroup>
</div>
);
};
return (
<Dropdown overlay={renderItems()} trigger={['click']}>
<div>
<span className="icon icon-arrow-down" />
<span className="title">{title}</span>
</div>
</Dropdown>
);
}
It looks like the only difference you are talking about making is using an array of objects instead of strings? If that's the case, when looping through the array to create the checkboxes, you access the object attributes using dot notation. It should look something like this if I understand the problem correctly.
From CheckboxGroup component:
this.props.options.forEach(el => {
let name = el.name;
let id = el.id;
//rest of code to create checkboxes
or to show an example in creating components
let checkboxMarkup = [];
checkboxMarkup.push(
<input type="checkbox" id={el.id} name={el.name} key={`${el.id} - ${el.name}`}/>
);
}
'el' in this case refers to each individual object when looping through the array. It's not necessary to assign it to a variable, I just used that to show an example of how to access the properties.