Skip to main content

第六节:实时航班及航班详情显示

前几节我们介绍了如何加载气象的实况和预报数据,本节我们将实现实时航班信息的显示,核心也是渲染矢量数据,但加入了定时刷新的功能。

添加相关菜单和业务逻辑

之前我们并没有在图层菜单上添加航班相关的信息,也没有创建相关占位的图层创建方法,现在我们先把这些加上去。

更新菜单

在菜单中加入航班相关的按钮项即可:

public menuData:IMenuItem[]=[
...,
  {
	name: "实时航班",
	id: "flight",
	userData: {
	    overlay: true
	}
  }
];
航班信息是允许叠加在其他图层之上的,所以我们设置自定义的overlay为true:

图层创建方法

我们在图层管理器的 createLayer方法中加入新的航班点击的判断:


private async _createFlightLayer() {
    return undefined;
}

public async createLayer(item: IMenuItem): Promise<boolean> {
    ...
    let layer: L.Layer;
    if (item.id === "realtime_metar") {
        layer = await this._createMetarLayer();
    } else if (item.id === "realtime_sate") {
        layer = this._createCloudLayer();
    } else if (item.id.startsWith("gfs")) {
        layer = await this._createGFSLayer(item.userData.dataInfo);
    } else if (item.id === "flight") {
        layer = await this._createFlightLayer();
    } else {
        //TODO other layers
    }
    ...
}

航班信息获取

跟前几节一样,我们首先要寻找数据从哪里来。

数据来源

本着使用现有免费数据的原则,我们这里使用外网免费公开的航班信息接口:

https://opensky-network.org/api/states/all?lamin=5&lomin=60&lamax=65&lomax=160

该接口返回指定范围内各个航班最近一次更新的位置、速度、航向等信息。

该接口为国外爱好者免费提供的接口,实时性和准确性未知,切勿做实际生产用途!

现在我们可以在创建航班图层的方法中添加数据获取的代码:

const data = await (await fetch("https://opensky-network.org/api/states/all?lamin=5&lomin=60&lamax=65&lomax=160")).json();

数据解析

我们将数据打印出来可以看到数据结构:

监测预报-第六节-航班信息打印.png

可以发现,返回的是一个json对象,包含了time和states两个字段,time是时间戳,而数据就是以states数组进行存储。

我们前面说过,以数组进行存储的数据,我们可以使用PointArrayFeatureProvider这个数据解析器来进行解析,但是这个解析器要求数组里面是一个个的对象,具有key-value形式,这主要是GeoJSON格式的properties要求,所以,我们有必要对这个数据进行一些简单的处理,加上每个字段的字段名,这就需要先加一个数组和字段名的映射,因此在图层管理器中定义一个局部变量(数据来自接口网站的说明):

private _flightPropsMap = [
        "icao24", "callsign", "origin_country", "time_position", "last_contact", "longitude", "latitude", "baro_altitude", "on_ground", "velocity", "true_track", "vertical_rate", "sensors", "geo_altitude", "squawk", "spi", "position_source"
    ];

该数组跟返回数据中的数值顺序一一对应。

另外,咱们航班数据后续是需要按指定间隔更新的,所以我们一开始就可以把获取航班数据这个提取为一个方法,第一次和后面每次更新都是调用这个方法来获取最新的数据:

private async _createFlightLayer() {
    const updateData = async () => {
        const data = await (await fetch("https://opensky-network.org/api/states/all?lamin=5&lomin=60&lamax=65&lomax=160")).json();
        const dataArr = [];
        for (const state of data.states) {
            const obj = {};
            for (let pIdx = 0; pIdx < state.length; pIdx++) {
                const p = this._flightPropsMap[pIdx];
                obj[p] = state[pIdx];
            }
            dataArr.push(obj);
        }
        const provider = new PointArrayFeatureProvider(dataArr, {
            lonField: this._flightPropsMap[5],
            latField: this._flightPropsMap[6],
            idField: this._flightPropsMap[0]
        });
        return provider;
    };
    const provider = await updateData();
    return undefined;
}

这样我们就完成了数据解析的工作。

样式设置

在创建图层进行显示之前,我们要设置数据需要显示的样式,这跟前面渲染Metar报文图层的思路是完全一致的:

  • 使用一个小飞机图标表示飞机当前的位置
  • 根据航向角度设置飞机图标的旋转角度

这个样式比显示站点填图的样式要简单很多!

在创建航班图层的方法中继续增加以下代码:

...
const style: IFeatureStyleOptions = {
    point: {
        visible: false,
        label: [
            {
                image: {
                    data: "plane#res",
                    offset: [-16, -16],
                    size: [32, 32],
                    angle: "$true_track#degree2arc"
                }
            }
        ]
    }
}
  • 这里我们设置默认的小圆点不可见,因为我们只需要显示飞机的图标
  • 另外在标签中增加了一个图标,图片来自预加载的资源,资源名称是plane。
  • 设置了图片大小的一半作为偏移量,这样可以保证图片的中心点和飞机的位置吻合(默认图片是原点是左上角)
  • 设置了图片的旋转角度从true_track字段获取,并且使用degree2arc这个loader将角度转换为弧度

创建图层

数据源和样式都准备好,下面就可以创建图层了,继续在上面的方法中加入以下代码:

const layer = new LGeoJSONLayer({
    pane: consts.customPanes.station.name,
    pickType: "point"
})
    .setDataSource(provider)
    .setDrawOptions(style);
return layer;

这时候刷新页面,点击航班图层,我们就可以看到一个个小飞机显示在地图上了:

监测预报-第六节-航班显示.png

添加航班信息拾取

我们现在只是显示了航班的位置和方向,对于接口返回的其他信息并没有使用,现在我们可以通过用户点击飞机图标,来弹出一个popup,里面显示航班的具体信息。

与Metar报文显示中一样,我们通过监听图层的拾取消息来实现这一功能,将创建航班图层代码更新如下:

private async _createFlightLayer() {
    ...
    const layer = new LGeoJSONLayer({
        pane: consts.customPanes.station.name,
        pickType: "point"
    })
        .setDataSource(provider)
        .setDrawOptions(style);
    layer.on(LGeoJSONLayer.EventTypes.picked, args => {
        if (!args["data"].length) {
            return;
        }
        const feature = args["data"][0];
        const props = feature.properties;
        let tip = "";
        for (const key in props) {
            tip += key + ":" + props[key] + "<br/>";
        }
        map.openPopup(tip, L.latLng(props.latitude, props.longitude));
    });
    return layer;
}

现在点击小飞机,就会出现一个详细的航班详情窗口了:

监测预报-第六节-航班信息详情.png

定时更新数据

飞机飞行的数据是不断更新的,理论上我们是可以实时刷新的,但是由于免费接口限制了调用评率,而且外网调用速度较慢,我们这里改为定时刷新。

这也是与之前所有数据存在差别的一点(也可以将站点观测数据升级为定时刷新,感兴趣的同学可以自己尝试)。

设置计时器标记

这个功能可以通过setInterval的定时器实现,setInterval返回一个定时器的标记,当我们需要取消定时的时候可以将这个标记传入clearInterval方法。当我们取消显示航班图层的时候,就需要取消数据的更新,因此,首先要在图层管理器中定义一个私有变量,保存这个定时器的标记值:

private _flightUpdater: any;

创建定时器

接下来就可以在航班图层创建函数中创建定时器并调用上面封装好的数据更新函数,获得最新的数据之后更新图层的数据源,将代码更新如下:

private async _createFlightLayer() {
    ...
    layer.on(LGeoJSONLayer.EventTypes.picked, args => {
        if (!args["data"].length) {
            return;
        }
        const feature = args["data"][0];
        const props = feature.properties;
        let tip = "";
        for (const key in props) {
            tip += key + ":" + props[key] + "<br/>";
        }
        map.openPopup(tip, L.latLng(props.latitude, props.longitude));
    });
    this._flightUpdater = setInterval(async () => {
        const provider = await updateData();
        let layer: LGeoJSONLayer = this._layers["flight"] as any;
        if (!layer) {
            return;
        }
        layer.setDataSource(provider);
    }, 30000);
    return layer;
}

这里我们设置了30秒更新一次航班信息,现在只要页面保持打开状态,航班信息就会持续更新,注意在上方的代码中,我们使用了layer.setDataSource(provider);的方式来更新数据源,这个方法如果传入的provider和之前的provider是同一个对象,但是内容变化了,就需要手动调用layer.redraw()方法重绘,如果不是同一个对象,则会自动重绘。

最后,更新图层的删除方法,在删除的时候,如果发现有定时器则删除(也可以监听图层的移除消息,在移除的时候自动取消更新):

public removeLayer(item: IMenuItem) {
    if (this._layers[item.id]) {
        this._layers[item.id].remove();
        delete this._layers[item.id];
    }
    if (item.id === "flight" && defined(this._flightUpdater)) {
        clearInterval(this._flightUpdater);
        this._flightUpdater = undefined;
    }
}

总结

本节的内容相对较为简单,但也是一个完整的创建菜单、获取数据、设置样式、创建图层的过程,而且还支持了图层的自动刷新!

完整代码及效果