mirror of
https://github.com/ethan-zf/cesium-plot-js.git
synced 2025-06-24 03:27:29 +00:00
To incorporate double-arrow animation.
This commit is contained in:
parent
c23c9aa654
commit
b234fca7dd
@ -47,6 +47,7 @@ const editStartHandler = () => {
|
|||||||
const editEndHandler = (geometryPoints: any) => {
|
const editEndHandler = (geometryPoints: any) => {
|
||||||
console.error('editEnd', geometryPoints);
|
console.error('editEnd', geometryPoints);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonGroup = document.getElementById('button-group') as HTMLElement;
|
const buttonGroup = document.getElementById('button-group') as HTMLElement;
|
||||||
buttonGroup.onclick = (evt) => {
|
buttonGroup.onclick = (evt) => {
|
||||||
const targetElement = evt.target as HTMLElement;
|
const targetElement = evt.target as HTMLElement;
|
||||||
@ -160,6 +161,12 @@ buttonGroup.onclick = (evt) => {
|
|||||||
geometry.off('editEnd', editEndHandler);
|
geometry.off('editEnd', editEndHandler);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'startGrowthAnimation':
|
||||||
|
if (geometry) {
|
||||||
|
geometry.startGrowthAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
<button id="remove">删除</button>
|
<button id="remove">删除</button>
|
||||||
<button id="addEvent">绑定事件</button>
|
<button id="addEvent">绑定事件</button>
|
||||||
<button id="removeEvent">解绑事件</button>
|
<button id="removeEvent">解绑事件</button>
|
||||||
|
<button id="startGrowthAnimation">生长动画</button>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
window.CESIUM_BASE_URL = 'node_modules/cesium/Build/Cesium';
|
window.CESIUM_BASE_URL = 'node_modules/cesium/Build/Cesium';
|
||||||
|
@ -15,6 +15,12 @@ export default class DoubleArrow extends Base {
|
|||||||
neckHeightFactor: number;
|
neckHeightFactor: number;
|
||||||
connPoint: Position;
|
connPoint: Position;
|
||||||
tempPoint4: Position;
|
tempPoint4: Position;
|
||||||
|
minPointsForShape: number;
|
||||||
|
llBodyPnts: Position[] = [];
|
||||||
|
rrBodyPnts: Position[] = [];
|
||||||
|
curveControlPointLeft: Cartesian3;
|
||||||
|
curveControlPointRight: Cartesian3;
|
||||||
|
isClockWise: boolean;
|
||||||
|
|
||||||
constructor(cesium: any, viewer: any, style?: PolygonStyle) {
|
constructor(cesium: any, viewer: any, style?: PolygonStyle) {
|
||||||
super(cesium, viewer, style);
|
super(cesium, viewer, style);
|
||||||
@ -25,6 +31,7 @@ export default class DoubleArrow extends Base {
|
|||||||
this.neckWidthFactor = 0.15;
|
this.neckWidthFactor = 0.15;
|
||||||
this.connPoint = [0, 0];
|
this.connPoint = [0, 0];
|
||||||
this.tempPoint4 = [0, 0];
|
this.tempPoint4 = [0, 0];
|
||||||
|
this.minPointsForShape = 4;
|
||||||
this.setState('drawing');
|
this.setState('drawing');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +49,30 @@ export default class DoubleArrow extends Base {
|
|||||||
} else if (this.points.length === 2) {
|
} else if (this.points.length === 2) {
|
||||||
this.setGeometryPoints(this.points);
|
this.setGeometryPoints(this.points);
|
||||||
this.drawPolygon();
|
this.drawPolygon();
|
||||||
} else if (this.points.length === 4) {
|
} else if (this.points.length === 3) {
|
||||||
|
this.lineEntity && this.viewer.entities.remove(this.lineEntity);
|
||||||
|
} else {
|
||||||
this.finishDrawing();
|
this.finishDrawing();
|
||||||
|
this.curveControlPointLeft = this.cesium.Cartesian3.fromDegrees(this.llBodyPnts[2][0], this.llBodyPnts[2][1]);
|
||||||
|
this.curveControlPointRight = this.cesium.Cartesian3.fromDegrees(this.rrBodyPnts[1][0], this.rrBodyPnts[1][1]);
|
||||||
|
|
||||||
|
// 辅助查看插值控制点位置
|
||||||
|
// this.CesiumViewer.entities.add({
|
||||||
|
// position: this.curveControlPointLeft,
|
||||||
|
// point: {
|
||||||
|
// pixelSize: 10,
|
||||||
|
// heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
// color: this.cesium.Color.RED,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// this.CesiumViewer.entities.add({
|
||||||
|
// position: this.curveControlPointRight,
|
||||||
|
// point: {
|
||||||
|
// pixelSize: 10,
|
||||||
|
// heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
// color: this.cesium.Color.RED,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -55,7 +84,7 @@ export default class DoubleArrow extends Base {
|
|||||||
if (tempPoints.length === 2) {
|
if (tempPoints.length === 2) {
|
||||||
this.addFirstLineOfTheArrow();
|
this.addFirstLineOfTheArrow();
|
||||||
} else if (tempPoints.length > 2) {
|
} else if (tempPoints.length > 2) {
|
||||||
const geometryPoints = this.createPolygon(tempPoints);
|
const geometryPoints = this.createGraphic(tempPoints);
|
||||||
this.setGeometryPoints(geometryPoints);
|
this.setGeometryPoints(geometryPoints);
|
||||||
this.drawPolygon();
|
this.drawPolygon();
|
||||||
}
|
}
|
||||||
@ -66,7 +95,7 @@ export default class DoubleArrow extends Base {
|
|||||||
*/
|
*/
|
||||||
updateDraggingPoint(cartesian: Cartesian3, index: number) {
|
updateDraggingPoint(cartesian: Cartesian3, index: number) {
|
||||||
this.points[index] = cartesian;
|
this.points[index] = cartesian;
|
||||||
const geometryPoints = this.createPolygon(this.points);
|
const geometryPoints = this.createGraphic(this.points);
|
||||||
this.setGeometryPoints(geometryPoints);
|
this.setGeometryPoints(geometryPoints);
|
||||||
this.drawPolygon();
|
this.drawPolygon();
|
||||||
}
|
}
|
||||||
@ -74,7 +103,7 @@ export default class DoubleArrow extends Base {
|
|||||||
/**
|
/**
|
||||||
* Generate geometric shapes based on key points.
|
* Generate geometric shapes based on key points.
|
||||||
*/
|
*/
|
||||||
createPolygon(positions: Cartesian3[]) {
|
createGraphic(positions: Cartesian3[]) {
|
||||||
const lnglatPoints = positions.map((pnt) => {
|
const lnglatPoints = positions.map((pnt) => {
|
||||||
return this.cartesianToLnglat(pnt);
|
return this.cartesianToLnglat(pnt);
|
||||||
});
|
});
|
||||||
@ -92,7 +121,8 @@ export default class DoubleArrow extends Base {
|
|||||||
}
|
}
|
||||||
let leftArrowPnts: Position[];
|
let leftArrowPnts: Position[];
|
||||||
let rightArrowPnts;
|
let rightArrowPnts;
|
||||||
if (Utils.isClockWise(pnt1, pnt2, pnt3)) {
|
this.isClockWise = Utils.isClockWise(pnt1, pnt2, pnt3);
|
||||||
|
if (this.isClockWise) {
|
||||||
leftArrowPnts = this.getArrowPoints(pnt1, this.connPoint, this.tempPoint4, false);
|
leftArrowPnts = this.getArrowPoints(pnt1, this.connPoint, this.tempPoint4, false);
|
||||||
rightArrowPnts = this.getArrowPoints(this.connPoint, pnt2, pnt3, true);
|
rightArrowPnts = this.getArrowPoints(this.connPoint, pnt2, pnt3, true);
|
||||||
} else {
|
} else {
|
||||||
@ -104,9 +134,11 @@ export default class DoubleArrow extends Base {
|
|||||||
const llBodyPnts = leftArrowPnts.slice(0, t);
|
const llBodyPnts = leftArrowPnts.slice(0, t);
|
||||||
const lArrowPnts = leftArrowPnts.slice(t, t + 5);
|
const lArrowPnts = leftArrowPnts.slice(t, t + 5);
|
||||||
let lrBodyPnts = leftArrowPnts.slice(t + 5, m);
|
let lrBodyPnts = leftArrowPnts.slice(t + 5, m);
|
||||||
|
this.llBodyPnts = llBodyPnts;
|
||||||
let rlBodyPnts = rightArrowPnts.slice(0, t);
|
let rlBodyPnts = rightArrowPnts.slice(0, t);
|
||||||
const rArrowPnts = rightArrowPnts.slice(t, t + 5);
|
const rArrowPnts = rightArrowPnts.slice(t, t + 5);
|
||||||
const rrBodyPnts = rightArrowPnts.slice(t + 5, m);
|
const rrBodyPnts = rightArrowPnts.slice(t + 5, m);
|
||||||
|
this.rrBodyPnts = rrBodyPnts;
|
||||||
rlBodyPnts = Utils.getBezierPoints(rlBodyPnts);
|
rlBodyPnts = Utils.getBezierPoints(rlBodyPnts);
|
||||||
const bodyPnts = Utils.getBezierPoints(rrBodyPnts.concat(llBodyPnts.slice(1)));
|
const bodyPnts = Utils.getBezierPoints(rrBodyPnts.concat(llBodyPnts.slice(1)));
|
||||||
lrBodyPnts = Utils.getBezierPoints(lrBodyPnts);
|
lrBodyPnts = Utils.getBezierPoints(lrBodyPnts);
|
||||||
@ -220,4 +252,16 @@ export default class DoubleArrow extends Base {
|
|||||||
getPoints() {
|
getPoints() {
|
||||||
return this.points;
|
return this.points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBezierControlPointforGrowthAnimation() {
|
||||||
|
return this.isClockWise
|
||||||
|
? {
|
||||||
|
left: this.curveControlPointLeft,
|
||||||
|
right: this.curveControlPointRight,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
right: this.curveControlPointLeft,
|
||||||
|
left: this.curveControlPointRight,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
175
src/base.ts
175
src/base.ts
@ -1,5 +1,4 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// import * as CesiumTypeOnly from 'cesium';
|
|
||||||
import * as CesiumTypeOnly from 'cesium';
|
import * as CesiumTypeOnly from 'cesium';
|
||||||
import {
|
import {
|
||||||
State,
|
State,
|
||||||
@ -9,10 +8,12 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
EventListener,
|
EventListener,
|
||||||
VisibleAnimationOpts,
|
VisibleAnimationOpts,
|
||||||
|
GrowthAnimationOpts,
|
||||||
} from './interface';
|
} from './interface';
|
||||||
import EventDispatcher from './events';
|
import EventDispatcher from './events';
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import merge from 'lodash.merge';
|
// import merge from 'lodash.merge';
|
||||||
|
import * as Utils from './utils';
|
||||||
|
|
||||||
export default class Base {
|
export default class Base {
|
||||||
cesium: typeof CesiumTypeOnly;
|
cesium: typeof CesiumTypeOnly;
|
||||||
@ -34,6 +35,7 @@ export default class Base {
|
|||||||
points: CesiumTypeOnly.Cartesian3[] = [];
|
points: CesiumTypeOnly.Cartesian3[] = [];
|
||||||
isHidden: boolean = false;
|
isHidden: boolean = false;
|
||||||
styleCache: GeometryStyle | undefined;
|
styleCache: GeometryStyle | undefined;
|
||||||
|
minPointsForShape: number = 0;
|
||||||
|
|
||||||
constructor(cesium: CesiumTypeOnly, viewer: CesiumTypeOnly.Viewer, style?: GeometryStyle) {
|
constructor(cesium: CesiumTypeOnly, viewer: CesiumTypeOnly.Viewer, style?: GeometryStyle) {
|
||||||
this.cesium = cesium;
|
this.cesium = cesium;
|
||||||
@ -642,6 +644,170 @@ export default class Base {
|
|||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startGrowthAnimation(opts: GrowthAnimationOpts) {
|
||||||
|
const { duration = 3000, delay = 0, callback } = opts || {};
|
||||||
|
if (this.state !== 'static') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.minPointsForShape) {
|
||||||
|
console.warn('Growth animation is not supported for this type of shape');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState('animating');
|
||||||
|
if (this.minPointsForShape === 4) {
|
||||||
|
// For double arrows, special handling is required.
|
||||||
|
this.doubleArrowGrowthAnimation(duration, delay, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hideWithAnimation(0, 0, undefined);
|
||||||
|
const points = this.getPoints();
|
||||||
|
|
||||||
|
let segmentDuration = 0;
|
||||||
|
if (this.minPointsForShape === 2) {
|
||||||
|
segmentDuration = duration / (points.length - 1);
|
||||||
|
} else {
|
||||||
|
segmentDuration = duration / (points.length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime = Date.now();
|
||||||
|
let movingPointIndex = 0;
|
||||||
|
this.viewer.clock.shouldAnimate = true;
|
||||||
|
|
||||||
|
const frameListener = (clock) => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const elapsedTime = currentTime - startTime;
|
||||||
|
if (elapsedTime >= duration) {
|
||||||
|
// Animation ends
|
||||||
|
callback && callback();
|
||||||
|
startTime = 0;
|
||||||
|
this.viewer.clock.shouldAnimate = false;
|
||||||
|
this.viewer.clock.onTick.removeEventListener(frameListener);
|
||||||
|
this.setState('static');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSegment = Math.floor(elapsedTime / segmentDuration);
|
||||||
|
let startPoint;
|
||||||
|
|
||||||
|
if (this.minPointsForShape === 2) {
|
||||||
|
movingPointIndex = currentSegment + 1;
|
||||||
|
} else {
|
||||||
|
movingPointIndex = currentSegment + 2;
|
||||||
|
}
|
||||||
|
startPoint = points[movingPointIndex - 1];
|
||||||
|
if (currentSegment == 0 && this.minPointsForShape === 3) {
|
||||||
|
// The face-arrow determined by three points, with the animation starting from the midpoint of the line connecting the first two points.
|
||||||
|
startPoint = this.cesium.Cartesian3.midpoint(points[0], points[1], new this.cesium.Cartesian3());
|
||||||
|
}
|
||||||
|
let endPoint = points[movingPointIndex];
|
||||||
|
// To dynamically add points between the startPoint and endPoint, consistent with the initial drawing logic,
|
||||||
|
// update the point at index movingPointIndex in the points array with the newPosition,
|
||||||
|
// generate the arrow, and execute the animation.
|
||||||
|
const t = (elapsedTime - currentSegment * segmentDuration) / segmentDuration;
|
||||||
|
const newPosition = this.cesium.Cartesian3.lerp(startPoint, endPoint, t, new this.cesium.Cartesian3());
|
||||||
|
const tempPoints = points.slice(0, movingPointIndex + 1);
|
||||||
|
tempPoints[tempPoints.length - 1] = newPosition;
|
||||||
|
const geometryPoints = this.createGraphic(tempPoints);
|
||||||
|
this.setGeometryPoints(geometryPoints);
|
||||||
|
this.showAnimation(0, 0, undefined);
|
||||||
|
};
|
||||||
|
this.viewer.clock.onTick.addEventListener(frameListener);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private doubleArrowGrowthAnimation(duration: number = 3000, delay: number = 0, callback?: Function) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.viewer.entities.remove(this.polygonEntity);
|
||||||
|
this.viewer.entities.remove(this.outlineEntity);
|
||||||
|
this.polygonEntity = null;
|
||||||
|
this.outlineEntity = null;
|
||||||
|
const points = this.getPoints();
|
||||||
|
let startTime = Date.now();
|
||||||
|
this.viewer.clock.shouldAnimate = true;
|
||||||
|
|
||||||
|
const frameListener = (clock) => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const elapsedTime = currentTime - startTime;
|
||||||
|
if (elapsedTime >= duration) {
|
||||||
|
// Animation ends
|
||||||
|
callback && callback();
|
||||||
|
startTime = 0;
|
||||||
|
this.viewer.clock.shouldAnimate = false;
|
||||||
|
this.viewer.clock.onTick.removeEventListener(frameListener);
|
||||||
|
this.setState('static');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utils.isClockWise(pnt1, pnt2, pnt3)
|
||||||
|
const midPoint = this.cesium.Cartesian3.midpoint(points[0], points[1], new this.cesium.Cartesian3());
|
||||||
|
|
||||||
|
const startPointLeft = this.cesium.Cartesian3.midpoint(points[0], midPoint, new this.cesium.Cartesian3());
|
||||||
|
|
||||||
|
const startPointRight = this.cesium.Cartesian3.midpoint(midPoint, points[1], new this.cesium.Cartesian3());
|
||||||
|
let endPointLeft = points[3];
|
||||||
|
let endPointRight = points[2];
|
||||||
|
const t = elapsedTime / duration;
|
||||||
|
const controlPoint = this.getBezierControlPointforGrowthAnimation();
|
||||||
|
let curveControlPointsLeft = [startPointLeft, controlPoint.left, endPointLeft];
|
||||||
|
let curveControlPointsRight = [startPointRight, controlPoint.right, endPointRight];
|
||||||
|
const newPositionLeft = this.getNewPosition(curveControlPointsLeft, t);
|
||||||
|
const newPositionRight = this.getNewPosition(curveControlPointsRight, t);
|
||||||
|
|
||||||
|
// Assist in viewing exercise routes
|
||||||
|
// this.viewer.entities.add({
|
||||||
|
// position: newPositionLeft,
|
||||||
|
// point: {
|
||||||
|
// pixelSize: 4,
|
||||||
|
// heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
// color: this.cesium.Color.RED,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// this.viewer.entities.add({
|
||||||
|
// position: newPositionRight,
|
||||||
|
// point: {
|
||||||
|
// pixelSize: 4,
|
||||||
|
// heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
// color: this.cesium.Color.RED,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
const tempPoints = [...points];
|
||||||
|
tempPoints[2] = newPositionRight;
|
||||||
|
tempPoints[3] = newPositionLeft;
|
||||||
|
const geometryPoints = this.createGraphic(tempPoints);
|
||||||
|
this.setGeometryPoints(geometryPoints);
|
||||||
|
this.drawPolygon();
|
||||||
|
};
|
||||||
|
this.viewer.clock.onTick.addEventListener(frameListener);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNewPosition(curveControlPoints, t) {
|
||||||
|
curveControlPoints = curveControlPoints.map((item) => {
|
||||||
|
return this.cartesianToLnglat(item);
|
||||||
|
});
|
||||||
|
let curvePoints = Utils.getCurvePoints(0.3, curveControlPoints);
|
||||||
|
curvePoints = curvePoints.map((p) => {
|
||||||
|
return this.cesium.Cartesian3.fromDegrees(p[0], p[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
let newPosition = this.interpolateAlongCurve(curvePoints, t);
|
||||||
|
return newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interpolateAlongCurve(curvePoints, t) {
|
||||||
|
const numPoints = curvePoints.length - 1;
|
||||||
|
const index = Math.floor(t * numPoints);
|
||||||
|
const tSegment = t * numPoints - index;
|
||||||
|
const startPoint = curvePoints[index];
|
||||||
|
const endPoint = curvePoints[index + 1];
|
||||||
|
const x = startPoint.x + (endPoint.x - startPoint.x) * tSegment;
|
||||||
|
const y = startPoint.y + (endPoint.y - startPoint.y) * tSegment;
|
||||||
|
const z = startPoint.z + (endPoint.z - startPoint.z) * tSegment;
|
||||||
|
|
||||||
|
return new this.cesium.Cartesian3(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
if (this.type === 'polygon') {
|
if (this.type === 'polygon') {
|
||||||
this.viewer.entities.remove(this.polygonEntity);
|
this.viewer.entities.remove(this.polygonEntity);
|
||||||
@ -692,4 +858,9 @@ export default class Base {
|
|||||||
return 'polygon';
|
return 'polygon';
|
||||||
//Abstract method that must be implemented by subclasses.
|
//Abstract method that must be implemented by subclasses.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createGraphic(points: CesiumTypeOnly.Cartesian3[]): CesiumTypeOnly.Cartesian3[] {
|
||||||
|
//Abstract method that must be implemented by subclasses.
|
||||||
|
return [new this.cesium.Cartesian3()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,20 @@ export type LineStyle = {
|
|||||||
lineWidth?: number;
|
lineWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type State = 'drawing' | 'edit' | 'static';
|
export type State = 'drawing' | 'edit' | 'static' | 'animating';
|
||||||
export type GeometryStyle = PolygonStyle | LineStyle;
|
export type GeometryStyle = PolygonStyle | LineStyle;
|
||||||
|
|
||||||
export type EventType = 'drawStart' | 'drawUpdate' | 'drawEnd' | 'editEnd' | 'editStart';
|
export type EventType = 'drawStart' | 'drawUpdate' | 'drawEnd' | 'editEnd' | 'editStart';
|
||||||
export type EventListener = (eventData?: any) => void;
|
export type EventListener = (eventData?: any) => void;
|
||||||
|
|
||||||
export type VisibleOpts = {
|
export type VisibleAnimationOpts = {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
callback?: (() => void)
|
callback?: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type GrowthAnimationOpts = {
|
||||||
|
duration: number;
|
||||||
|
delay: number;
|
||||||
|
callback: Function;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user