mirror of https://gitee.com/antv-l7/antv-l7
fix(district): fix polygon layer setData 样式不生效 & 增加下钻上取地图
This commit is contained in:
parent
6120d0904d
commit
b6a92b1994
|
@ -12,10 +12,16 @@ import { EventEmitter } from 'eventemitter3';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import geobuf from 'geobuf';
|
import geobuf from 'geobuf';
|
||||||
// tslint:disable-next-line: no-submodule-imports
|
// tslint:disable-next-line: no-submodule-imports
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/mergeWith';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Pbf from 'pbf';
|
import Pbf from 'pbf';
|
||||||
import { IDistrictLayerOption } from './interface';
|
import { IDistrictLayerOption } from './interface';
|
||||||
|
|
||||||
|
function mergeCustomizer(objValue: any, srcValue: any) {
|
||||||
|
if (Array.isArray(srcValue)) {
|
||||||
|
return srcValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
export default class BaseLayer extends EventEmitter {
|
export default class BaseLayer extends EventEmitter {
|
||||||
public fillLayer: ILayer;
|
public fillLayer: ILayer;
|
||||||
public lineLayer: ILayer;
|
public lineLayer: ILayer;
|
||||||
|
@ -28,7 +34,7 @@ export default class BaseLayer extends EventEmitter {
|
||||||
constructor(scene: Scene, option: Partial<IDistrictLayerOption> = {}) {
|
constructor(scene: Scene, option: Partial<IDistrictLayerOption> = {}) {
|
||||||
super();
|
super();
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.options = merge({}, this.getDefaultOption(), option);
|
this.options = merge(this.getDefaultOption(), option, mergeCustomizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
@ -82,8 +88,10 @@ export default class BaseLayer extends EventEmitter {
|
||||||
countyStroke: 'rgba(255,255,255,0.6)',
|
countyStroke: 'rgba(255,255,255,0.6)',
|
||||||
coastlineStroke: '#4190da',
|
coastlineStroke: '#4190da',
|
||||||
coastlineWidth: 1,
|
coastlineWidth: 1,
|
||||||
nationalStroke: 'gray',
|
nationalStroke: '#c994c7',
|
||||||
nationalWidth: 1,
|
nationalWidth: 0.5,
|
||||||
|
chinaNationalStroke: 'gray',
|
||||||
|
chinaNationalWidth: 1,
|
||||||
popup: {
|
popup: {
|
||||||
enable: true,
|
enable: true,
|
||||||
triggerEvent: 'mousemove',
|
triggerEvent: 'mousemove',
|
||||||
|
@ -156,6 +164,13 @@ export default class BaseLayer extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addLabelLayer(labelData: any, type: string = 'json') {
|
protected addLabelLayer(labelData: any, type: string = 'json') {
|
||||||
|
const labelLayer = this.addLabel(labelData, type);
|
||||||
|
this.scene.addLayer(labelLayer);
|
||||||
|
this.layers.push(labelLayer);
|
||||||
|
this.labelLayer = labelLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addLabel(labelData: any, type: string = 'json') {
|
||||||
const { label, zIndex } = this.options;
|
const { label, zIndex } = this.options;
|
||||||
const labelLayer = new PointLayer({
|
const labelLayer = new PointLayer({
|
||||||
zIndex: zIndex + 2,
|
zIndex: zIndex + 2,
|
||||||
|
@ -175,9 +190,7 @@ export default class BaseLayer extends EventEmitter {
|
||||||
strokeWidth: label.strokeWidth,
|
strokeWidth: label.strokeWidth,
|
||||||
textAllowOverlap: label.textAllowOverlap,
|
textAllowOverlap: label.textAllowOverlap,
|
||||||
});
|
});
|
||||||
this.scene.addLayer(labelLayer);
|
return labelLayer;
|
||||||
this.layers.push(labelLayer);
|
|
||||||
this.labelLayer = labelLayer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addPopup() {
|
protected addPopup() {
|
||||||
|
|
|
@ -17,7 +17,12 @@ export default class CountryLayer extends BaseLayer {
|
||||||
this.loadData().then(([fillData, fillLabel]) => {
|
this.loadData().then(([fillData, fillLabel]) => {
|
||||||
this.addFillLayer(fillData);
|
this.addFillLayer(fillData);
|
||||||
if (fillLabel && this.options.label?.enable) {
|
if (fillLabel && this.options.label?.enable) {
|
||||||
this.addLabelLayer(fillLabel);
|
this.addLabelLayer(
|
||||||
|
fillLabel.filter((v: any) => {
|
||||||
|
return v.name !== '澳门';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.addMCLabel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const countryConfig = DataConfig.country.CHN[depth];
|
const countryConfig = DataConfig.country.CHN[depth];
|
||||||
|
@ -88,6 +93,8 @@ export default class CountryLayer extends BaseLayer {
|
||||||
const {
|
const {
|
||||||
nationalStroke,
|
nationalStroke,
|
||||||
nationalWidth,
|
nationalWidth,
|
||||||
|
chinaNationalStroke,
|
||||||
|
chinaNationalWidth,
|
||||||
coastlineStroke,
|
coastlineStroke,
|
||||||
coastlineWidth,
|
coastlineWidth,
|
||||||
stroke,
|
stroke,
|
||||||
|
@ -105,7 +112,7 @@ export default class CountryLayer extends BaseLayer {
|
||||||
} else if (v === '2') {
|
} else if (v === '2') {
|
||||||
return coastlineWidth;
|
return coastlineWidth;
|
||||||
} else if (v === '0') {
|
} else if (v === '0') {
|
||||||
return nationalWidth;
|
return chinaNationalWidth;
|
||||||
} else {
|
} else {
|
||||||
return '#fff';
|
return '#fff';
|
||||||
}
|
}
|
||||||
|
@ -117,7 +124,7 @@ export default class CountryLayer extends BaseLayer {
|
||||||
} else if (v === '2') {
|
} else if (v === '2') {
|
||||||
return coastlineStroke;
|
return coastlineStroke;
|
||||||
} else if (v === '0') {
|
} else if (v === '0') {
|
||||||
return nationalStroke;
|
return chinaNationalStroke;
|
||||||
} else {
|
} else {
|
||||||
return '#fff';
|
return '#fff';
|
||||||
}
|
}
|
||||||
|
@ -158,7 +165,6 @@ export default class CountryLayer extends BaseLayer {
|
||||||
|
|
||||||
// 县级边界
|
// 县级边界
|
||||||
private async addCountryBorder(cfg: any) {
|
private async addCountryBorder(cfg: any) {
|
||||||
// const bordConfig = DataConfig.country.CHN[3];
|
|
||||||
const border1 = await this.fetchData(cfg);
|
const border1 = await this.fetchData(cfg);
|
||||||
const { countyStrokeWidth, countyStroke } = this.options;
|
const { countyStrokeWidth, countyStroke } = this.options;
|
||||||
const cityline = new LineLayer({
|
const cityline = new LineLayer({
|
||||||
|
@ -173,4 +179,48 @@ export default class CountryLayer extends BaseLayer {
|
||||||
this.scene.addLayer(cityline);
|
this.scene.addLayer(cityline);
|
||||||
this.layers.push(cityline);
|
this.layers.push(cityline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addMCLabel() {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
name: '澳门',
|
||||||
|
center: [113.537747, 22.187009],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const labelLayer1 = this.addText(data, { maxZoom: 3 }, [-45, -10]);
|
||||||
|
const labelLayer2 = this.addText(data, { minZoom: 3, maxZoom: 4 }, [
|
||||||
|
-35,
|
||||||
|
-10,
|
||||||
|
]);
|
||||||
|
const labelLayer = this.addText(data, { minZoom: 4 }, [0, 0]);
|
||||||
|
this.scene.addLayer(labelLayer);
|
||||||
|
this.scene.addLayer(labelLayer1);
|
||||||
|
this.scene.addLayer(labelLayer2);
|
||||||
|
this.layers.push(labelLayer, labelLayer1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addText(labelData: any, option: any, offset: [number, number]) {
|
||||||
|
const { label, zIndex } = this.options;
|
||||||
|
const labelLayer = new PointLayer({
|
||||||
|
zIndex: zIndex + 2,
|
||||||
|
...option,
|
||||||
|
})
|
||||||
|
.source(labelData, {
|
||||||
|
parser: {
|
||||||
|
type: 'json',
|
||||||
|
coordinates: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.color(label.color as StyleAttrField)
|
||||||
|
.shape(label.field as StyleAttrField, 'text')
|
||||||
|
.size(10)
|
||||||
|
.style({
|
||||||
|
opacity: label.opacity,
|
||||||
|
stroke: label.stroke,
|
||||||
|
strokeWidth: label.strokeWidth,
|
||||||
|
textAllowOverlap: label.textAllowOverlap,
|
||||||
|
textOffset: offset,
|
||||||
|
});
|
||||||
|
return labelLayer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,30 +8,71 @@ export default class DrillDownLayer {
|
||||||
private provinceLayer: ProvinceLayer;
|
private provinceLayer: ProvinceLayer;
|
||||||
private cityLayer: CityLayer;
|
private cityLayer: CityLayer;
|
||||||
private countryLayer: CountryLayer;
|
private countryLayer: CountryLayer;
|
||||||
|
private scene: Scene;
|
||||||
constructor(scene: Scene, option: Partial<IDistrictLayerOption>) {
|
constructor(scene: Scene, option: Partial<IDistrictLayerOption>) {
|
||||||
const cfg = this.getDefaultOption();
|
const cfg = this.getDefaultOption();
|
||||||
|
this.scene = scene;
|
||||||
this.countryLayer = new CountryLayer(scene, option);
|
this.countryLayer = new CountryLayer(scene, option);
|
||||||
this.provinceLayer = new ProvinceLayer(scene, cfg.city);
|
this.provinceLayer = new ProvinceLayer(scene, cfg.city);
|
||||||
// this.cityLayer = new CityLayer(scene);
|
this.cityLayer = new CityLayer(scene, cfg.county);
|
||||||
// this.provinceLayer.hide();
|
this.scene.setMapStatus({ doubleClickZoom: false });
|
||||||
// this.cityLayer.hide();
|
|
||||||
this.countryLayer.on('loaded', () => {
|
this.countryLayer.on('loaded', () => {
|
||||||
|
this.addCountryEvent();
|
||||||
|
});
|
||||||
|
this.provinceLayer.on('loaded', () => {
|
||||||
this.addProvinceEvent();
|
this.addProvinceEvent();
|
||||||
});
|
});
|
||||||
|
this.cityLayer.on('loaded', () => {
|
||||||
|
this.addCityEvent();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public getDefaultOption() {
|
public getDefaultOption() {
|
||||||
return {
|
return {
|
||||||
province: {},
|
province: {},
|
||||||
city: {
|
city: {
|
||||||
adcode: '',
|
adcode: [],
|
||||||
},
|
},
|
||||||
county: {
|
county: {
|
||||||
adcode: [],
|
adcode: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
public addCountryEvent() {
|
||||||
|
this.countryLayer.fillLayer.on('click', (e: any) => {
|
||||||
|
this.countryLayer.hide();
|
||||||
|
// 更新市级行政区划
|
||||||
|
this.provinceLayer.updateDistrict([e.feature.properties.adcode]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public addProvinceEvent() {
|
public addProvinceEvent() {
|
||||||
// this.countryLayer.fillLayer.on('click', (e: any) => {
|
this.provinceLayer.fillLayer.on('undblclick', () => {
|
||||||
// });
|
this.countryLayer.show();
|
||||||
|
this.countryLayer.fillLayer.fitBounds();
|
||||||
|
this.provinceLayer.hide();
|
||||||
|
});
|
||||||
|
this.provinceLayer.fillLayer.on('click', (e: any) => {
|
||||||
|
this.provinceLayer.hide();
|
||||||
|
let adcode = e.feature.properties.adcode.toFixed(0);
|
||||||
|
if (adcode.substr(2, 2) === '00') {
|
||||||
|
adcode = adcode.substr(0, 2) + '0100';
|
||||||
|
}
|
||||||
|
// 更新县级行政区划
|
||||||
|
this.cityLayer.updateDistrict([adcode]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addCityEvent() {
|
||||||
|
this.cityLayer.fillLayer.on('undblclick', () => {
|
||||||
|
this.provinceLayer.show();
|
||||||
|
this.provinceLayer.fillLayer.fitBounds();
|
||||||
|
this.cityLayer.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.countryLayer.destroy();
|
||||||
|
this.provinceLayer.destroy();
|
||||||
|
this.cityLayer.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ export interface IDistrictLayerOption {
|
||||||
coastlineWidth: number;
|
coastlineWidth: number;
|
||||||
nationalStroke: string;
|
nationalStroke: string;
|
||||||
nationalWidth: number;
|
nationalWidth: number;
|
||||||
|
chinaNationalStroke: string;
|
||||||
|
chinaNationalWidth: number;
|
||||||
popup: Partial<{
|
popup: Partial<{
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
triggerEvent: 'mousemove' | 'click';
|
triggerEvent: 'mousemove' | 'click';
|
||||||
|
|
|
@ -13,7 +13,6 @@ export default class WorldLayer extends BaseLayer {
|
||||||
constructor(scene: Scene, option: Partial<IDistrictLayerOption> = {}) {
|
constructor(scene: Scene, option: Partial<IDistrictLayerOption> = {}) {
|
||||||
super(scene, option);
|
super(scene, option);
|
||||||
this.loadData().then(([fillData, lineData, fillLabel]) => {
|
this.loadData().then(([fillData, lineData, fillLabel]) => {
|
||||||
// this.addWorldBorder(border1, border2, island);
|
|
||||||
this.addFillLayer(fillData);
|
this.addFillLayer(fillData);
|
||||||
this.addFillLine(lineData);
|
this.addFillLine(lineData);
|
||||||
if (this.options.label?.enable) {
|
if (this.options.label?.enable) {
|
||||||
|
@ -28,18 +27,21 @@ export default class WorldLayer extends BaseLayer {
|
||||||
return (
|
return (
|
||||||
feature.properties.type === '10' ||
|
feature.properties.type === '10' ||
|
||||||
feature.properties.type === '1' ||
|
feature.properties.type === '1' ||
|
||||||
feature.properties.type === '11'
|
feature.properties.type === '11' ||
|
||||||
|
feature.properties.type === '8'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const bordFc = {
|
const bordFc = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: bord1,
|
features: bord1,
|
||||||
};
|
};
|
||||||
|
// 已确定国界
|
||||||
const nationalBorder = data.features.filter((feature: any) => {
|
const nationalBorder = data.features.filter((feature: any) => {
|
||||||
return (
|
return (
|
||||||
feature.properties.type !== '10' &&
|
feature.properties.type !== '10' &&
|
||||||
feature.properties.type !== '1' &&
|
feature.properties.type !== '1' &&
|
||||||
feature.properties.type !== '11'
|
feature.properties.type !== '11' &&
|
||||||
|
feature.properties.type !== '8'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const nationalFc = {
|
const nationalFc = {
|
||||||
|
@ -62,6 +64,8 @@ export default class WorldLayer extends BaseLayer {
|
||||||
nationalStroke,
|
nationalStroke,
|
||||||
nationalWidth,
|
nationalWidth,
|
||||||
coastlineStroke,
|
coastlineStroke,
|
||||||
|
chinaNationalStroke,
|
||||||
|
chinaNationalWidth,
|
||||||
coastlineWidth,
|
coastlineWidth,
|
||||||
zIndex,
|
zIndex,
|
||||||
} = this.options;
|
} = this.options;
|
||||||
|
@ -73,15 +77,13 @@ export default class WorldLayer extends BaseLayer {
|
||||||
.size(0.6)
|
.size(0.6)
|
||||||
.color('type', (v: string) => {
|
.color('type', (v: string) => {
|
||||||
if (v === '0') {
|
if (v === '0') {
|
||||||
return 'rgb(99,100, 99)'; // 中国国界线
|
return chinaNationalStroke; // 中国国界线
|
||||||
} else if (v === '2') {
|
} else if (v === '2' || v === '9') {
|
||||||
return 'rgb(0,136, 191)'; // 中国海岸线
|
return coastlineStroke; // 中国海岸线
|
||||||
} else if (v === '9') {
|
|
||||||
return 'rgb(0,136, 191)'; // 国外海岸线
|
|
||||||
} else if (v === '7') {
|
} else if (v === '7') {
|
||||||
return '#9ecae1'; // 国外国界
|
return nationalStroke; // 国外国界
|
||||||
} else {
|
} else {
|
||||||
return '#9ecae1';
|
return nationalStroke;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 添加未定国界
|
// 添加未定国界
|
||||||
|
@ -89,13 +91,19 @@ export default class WorldLayer extends BaseLayer {
|
||||||
zIndex: zIndex + 1,
|
zIndex: zIndex + 1,
|
||||||
})
|
})
|
||||||
.source(boundaries2)
|
.source(boundaries2)
|
||||||
.size(nationalWidth)
|
.size('type', (v: string) => {
|
||||||
|
if (v === '1') {
|
||||||
|
return chinaNationalWidth;
|
||||||
|
} else {
|
||||||
|
return nationalWidth;
|
||||||
|
}
|
||||||
|
})
|
||||||
.shape('line')
|
.shape('line')
|
||||||
.color('type', (v: string) => {
|
.color('type', (v: string) => {
|
||||||
if (v === '1') {
|
if (v === '1') {
|
||||||
return 'rgb(99,100, 99)';
|
return chinaNationalStroke;
|
||||||
} else {
|
} else {
|
||||||
return '#9ecae1';
|
return nationalStroke;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.style({
|
.style({
|
||||||
|
|
|
@ -684,6 +684,10 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
|
||||||
}
|
}
|
||||||
const source = this.getSource();
|
const source = this.getSource();
|
||||||
const extent = source.extent;
|
const extent = source.extent;
|
||||||
|
const isValid = extent.some((v) => Math.abs(v) === Infinity);
|
||||||
|
if (isValid) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
this.mapService.fitBounds(
|
this.mapService.fitBounds(
|
||||||
[
|
[
|
||||||
[extent[0], extent[1]],
|
[extent[0], extent[1]],
|
||||||
|
|
|
@ -61,6 +61,9 @@ export default class FeatureScalePlugin implements ILayerPlugin {
|
||||||
this.scaleOptions = layer.getScaleOptions();
|
this.scaleOptions = layer.getScaleOptions();
|
||||||
const attributes = styleAttributeService.getLayerStyleAttributes();
|
const attributes = styleAttributeService.getLayerStyleAttributes();
|
||||||
const { dataArray } = layer.getSource().data;
|
const { dataArray } = layer.getSource().data;
|
||||||
|
if (dataArray.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.caculateScalesForAttributes(attributes || [], dataArray);
|
this.caculateScalesForAttributes(attributes || [], dataArray);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,6 +72,9 @@ export default class FeatureScalePlugin implements ILayerPlugin {
|
||||||
this.scaleOptions = layer.getScaleOptions();
|
this.scaleOptions = layer.getScaleOptions();
|
||||||
const attributes = styleAttributeService.getLayerStyleAttributes();
|
const attributes = styleAttributeService.getLayerStyleAttributes();
|
||||||
const { dataArray } = layer.getSource().data;
|
const { dataArray } = layer.getSource().data;
|
||||||
|
if (dataArray.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.caculateScalesForAttributes(attributes || [], dataArray);
|
this.caculateScalesForAttributes(attributes || [], dataArray);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -78,6 +84,9 @@ export default class FeatureScalePlugin implements ILayerPlugin {
|
||||||
const attributes = styleAttributeService.getLayerStyleAttributes();
|
const attributes = styleAttributeService.getLayerStyleAttributes();
|
||||||
if (attributes) {
|
if (attributes) {
|
||||||
const { dataArray } = layer.getSource().data;
|
const { dataArray } = layer.getSource().data;
|
||||||
|
if (dataArray.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const attributesToRescale = attributes.filter(
|
const attributesToRescale = attributes.filter(
|
||||||
(attribute) => attribute.needRescale,
|
(attribute) => attribute.needRescale,
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Country extends React.Component {
|
||||||
pitch: 0,
|
pitch: 0,
|
||||||
style: 'blank',
|
style: 'blank',
|
||||||
zoom: 3,
|
zoom: 3,
|
||||||
minZoom: 3,
|
minZoom: 0,
|
||||||
maxZoom: 10,
|
maxZoom: 10,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -112,8 +112,12 @@ export default class TextLayerDemo extends React.Component {
|
||||||
});
|
});
|
||||||
scene.on('loaded', () => {
|
scene.on('loaded', () => {
|
||||||
const layer = new PolygonLayer({})
|
const layer = new PolygonLayer({})
|
||||||
.source(data)
|
.source({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
})
|
||||||
.shape('fill')
|
.shape('fill')
|
||||||
|
// .color('red')
|
||||||
.color('childrenNum', [
|
.color('childrenNum', [
|
||||||
'rgb(247,252,240)',
|
'rgb(247,252,240)',
|
||||||
'rgb(224,243,219)',
|
'rgb(224,243,219)',
|
||||||
|
@ -128,6 +132,10 @@ export default class TextLayerDemo extends React.Component {
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
});
|
});
|
||||||
scene.addLayer(layer);
|
scene.addLayer(layer);
|
||||||
|
setTimeout(() => {
|
||||||
|
layer.setData(data);
|
||||||
|
console.log('update');
|
||||||
|
}, 2000);
|
||||||
layer.on('click', (e) => {
|
layer.on('click', (e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue