客户需求
项目名称是:农作物长势遥感监测信息发布平台
- 农作物长势分析:做成下拉菜单,下拉菜单里面是月份。点击地块之后可以出现每个月份的长势图(是曲线图)
- 农作物分类:农作物共分了6类,也可以做成下拉菜单,点击菜单出现棉花,玉米等类,在点击下拉菜单的时候可以在图中显示对应的种类。
- 农作物面积:也是下拉菜单,也主要是棉花,玉米,小麦等5类作物面积。在点击下拉菜单时出现地块和面积统计数据。(要是地块数据不好显示可以只做显示面积数据)
- 用户在点击每个地块数据时可以给用户显示地块的种植类和面积已经属于那个乡镇
- 定位用户的地理位置,可以显示该位置的地块信息(如果是耕地就显示,不是耕地就不用显示了)
- 地图常用的放大缩小功能,和搜索查询功能
以上是原始需求,实现上做了一些改动
原始数据
软件安装
- 用于发布地图服务java环境:Tomcat + geoserver + jdk
- 用于后端python
- 用于前端nodejs
- 开发工具visual studio code、pycharm等
服务发布
简单发布shp 和 tif影像数据wms服务即可
源码
前端主要是react+dva+umi+antdesign框架
前端 核心源码 pages/openlayers_nongye/index.js
/* global mars3d Cesium*/
import React, { Component } from 'react';
import { connect } from 'dva';
import { Divider, Checkbox, Button, Row, Col, Tooltip, Modal, Select, Tree,Icon } from 'antd'
import styles from './style.less'
import echarts from 'echarts'
import Pie from './pie'
import Source from './source'
const { TreeNode } = Tree;
/* openlayers */
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import Feature from 'ol/Feature';
import { OSM, XYZ } from 'ol/source';
import { LineString, Point, Polygon } from 'ol/geom';
import { defaults as defaultInteractions, Pointer as PointerInteraction } from 'ol/interaction';
import { Image as ImageLayer,Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { ImageWMS ,TileJSON, Vector as VectorSource } from 'ol/source';
import { Fill, Stroke, Style } from 'ol/style';
import { toLonLat, fromLonLat, get } from 'ol/proj';
import { FullScreen } from 'ol/control';
import filter,{or,like,and, equalTo} from 'ol/format/filter';
import GeoJSON from 'ol/format/GeoJSON';
import { none } from 'ol/centerconstraint';
const chinaJson = require('./file/wuhan.geojson')
const { Option, OptGroup } = Select;
@connect(({ nongye }) => ({
nongye
}))
class Nongye extends Component {
constructor(props) {
super(props)
this.state = {
map: null,
chart: null,
//底图
digitalLayer: new TileLayer({
//source: new OSM()
source: new XYZ({
url: 'https://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
})
}),
imagesLayer: new TileLayer({
//source: new OSM()
source: new XYZ({
url: 'https://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=6&x={x}&y={y}&z={z}'
})
}),
arcgisLayer: new TileLayer({
source: new XYZ({
//url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png'
url: 'http://cache1.arcgisonline.cn/arcgis/rest/services/ChinaOnlineStreetPurplishBlue/MapServer/tile/{z}/{y}/{x}'
})
}),
//业务图层
wushuImage:new ImageLayer({
source: new ImageWMS({
url: '/nongye/geoserver/nongye/wms',
params: {'LAYERS': 'nongye:geotiff_coverage'},
ratio: 1,
serverType: 'geoserver',
crossOrigin: 'anonymous',
}),
}),
wushuBoundary:new ImageLayer({
source: new ImageWMS({
url: '/nongye/geoserver/nongye/wms',
params: {'LAYERS': 'nongye:wusubound'},
ratio: 1,
serverType: 'geoserver',
crossOrigin: 'anonymous',
}),
}),
wushuCrops:new ImageLayer({
source: new ImageWMS({
url: '/nongye/geoserver/nongye/wms',
params: {
'LAYERS': 'nongye:wusugd',
// 'CQL_FILTER': `kind='mianhua'`
},
ratio: 1,
serverType: 'geoserver',
crossOrigin: 'anonymous',
}),
}),
wuhanGeojson: new VectorLayer({
source: new VectorSource({
url: chinaJson,
format: new GeoJSON()
}),
style: new Style({
stroke: new Stroke({
color: '#319FD3',
width: 20
})
})
}),
showChart: false,
crop_prop:null,//点击地块详细信息
}
}
componentDidMount() {
this.start()
this.setState({
chart: this.refs.chart
})
}
start = () => {
this.initMap({
}).then(() => {
const {map,wushuCrops} = this.state
//监听动作
let that = this
map.on('singleclick', function (evt) {
// document.getElementById('info').innerHTML = '';
var viewResolution = /** @type {number} */ (map.getView().getResolution());
var url = wushuCrops.getSource().getFeatureInfoUrl(
evt.coordinate,
viewResolution,
'EPSG:3857',
{'INFO_FORMAT': 'application/json'}
);
if (url) {
fetch(url)
.then(function (response) { return response.text(); })
.then(function (result) {
console.log(result)
let json =JSON.parse(result)
that.setState({
showChart:false,
crop_prop:json.features.length>0 ?json.features[0].properties:null
})
});
}
});
map.on('pointermove', function (evt) {
if (evt.dragging) {
return;
}
var pixel = map.getEventPixel(evt.originalEvent);
var hit = map.forEachLayerAtPixel(pixel, function () {
return true;
});
map.getTargetElement().style.cursor = hit ? 'pointer' : '';
});
})
}
initMap = () => {
const { wuhanGeojson, arcgisLayer, imagesLayer, digitalLayer,wushuBoundary,wushuImage,wushuCrops } = this.state
arcgisLayer.set('id', 'arcgisLayer')
arcgisLayer.setVisible(false)
imagesLayer.set('id', 'imagesLayer')
imagesLayer.setVisible(false)
digitalLayer.set('id', 'digitalLayer')
digitalLayer.setVisible(true)
wushuImage.set("id",'wushuImage')
wushuImage.setVisible(false)
wushuBoundary.set("id",'wushuBoundary')
wushuBoundary.setVisible(false)
wushuCrops.set("id",'wushuCrops')
wushuCrops.setVisible(true)
return new Promise(resolve => {
var fullScreenControl = new FullScreen()
var map = new Map({
interactions: defaultInteractions().extend([new Drag()]),
view: new View({
center: fromLonLat([84.41,44.60]),
zoom: 9
}),
layers: [
arcgisLayer,
imagesLayer,
digitalLayer,
wuhanGeojson,
wushuImage,
wushuBoundary,
wushuCrops,
],
target: 'map'
});
map.addControl(fullScreenControl)
this.setState({
map
},()=>{
resolve()
})
})
};
//定位
location =()=>{
const {map} = this.state
let bmap = new BMap.Map("allmap");
var geolocation = new BMap.Geolocation();
geolocation.getCurrentPosition(function(r){
if(this.getStatus() == BMAP_STATUS_SUCCESS){
// alert('您的位置:'+r.point.lng+','+r.point.lat);
map.setView(new View({
center: fromLonLat([r.point.lng,r.point.lat]),
zoom: 10
}))
}
else {
alert('failed'+this.getStatus());
}
});
}
//影像和电子地图切换
anhei = () => {
const { map, arcgisLayer } = this.state
var layers = map.getLayers()
for (let i = 0; i < layers.getLength(); i++) {
var id = layers.item(i).get('id')
if (id === 'imagesLayer' || id === 'digitalLayer') {
layers.item(i).setVisible(false)
}
}
arcgisLayer.setVisible(true)
}
weixing = () => {
const { map, imagesLayer } = this.state
var layers = map.getLayers()
for (let i = 0; i < layers.getLength(); i++) {
var id = layers.item(i).get('id')
if (id === 'arcgisLayer' || id === 'digitalLayer') {
layers.item(i).setVisible(false)
}
}
imagesLayer.setVisible(true)
}
luwang = () => {
const { map, digitalLayer } = this.state
var layers = map.getLayers()
for (let i = 0; i < layers.getLength(); i++) {
var id = layers.item(i).get('id')
if (id === 'arcgisLayer' || id === 'imagesLayer') {
layers.item(i).setVisible(false)
}
}
digitalLayer.setVisible(true)
}
onCheck = (checkedKeys, info) => {
const {wushuCrops,wushuImage,wushuBoundary} = this.state
//显示隐藏图层数据
if(checkedKeys.includes("wushu")){//两者皆有
wushuImage.setVisible(true)
wushuBoundary.setVisible(true)
}else if (checkedKeys.includes("image")) { //只有影像
wushuImage.setVisible(true)
wushuBoundary.setVisible(false)
}else if(checkedKeys.includes("boundary")){ //只有边界
wushuImage.setVisible(false)
wushuBoundary.setVisible(true)
}else{
wushuImage.setVisible(false)
wushuBoundary.setVisible(false)
}
//过滤crops业务图层要素数据
let p = ''
checkedKeys.forEach(element => {
p += "'"+element+"' ,"
});
wushuCrops.getSource().updateParams({
'CQL_FILTER': `kind in ${'('+p.substr(0,p.length-1)+')'}`
})
wushuCrops.getSource().refresh()
};
getArrDifference =(arr1, arr2)=> {
return arr1.concat(arr2).filter(function(v, i, arr) {
return arr.indexOf(v) === arr.lastIndexOf(v);
});
}
tongji =()=>{
const {crop_prop } = this.state
this.setState({
showChart:true
},()=>{
getFromCache("tongjiArea").then(data=>{
if(!data){
this.props.dispatch({
type: 'nongye/getTongjiArea',
payload: {},
}).then(result => {
setToCache("tongjiArea",result)
this.initBar(result.data)
})
}else{
this.initBar(data.data)
}
})
})
}
initBar = (data)=>{
var myChart = echarts.init(document.getElementById('chart'));
var option = {
title: {
text: '农作物面积统计(平方米)'
},
tooltip: {},
xAxis: {
type: 'category',
data: ['棉花', '番茄', '蔬菜','小麦','玉米','其他']
},
yAxis: {
type: 'value'
},
grid: {
bottom: '10%',
left: '3%',
containLabel: true
},
series: [{
data: [data['mianhua'],data['fanqie'],data['shucai'],data['xiaomai'],data['yumi'],data['qita']],
type: 'bar'
}]
};
myChart.setOption(option);
}
zhangshi = () =>{
const {crop_prop } = this.state
this.setState({
showChart:true
},()=>{
var myChart = echarts.init(document.getElementById('chart'));
var option = {
title: {
text: '长势'
},
tooltip: {},
xAxis: {
type: 'category',
data: ['3月', '5月', '6月','7月','8月','9月','10月']
},
yAxis: {
type: 'value'
},
grid: {
bottom: '10%',
containLabel: true
},
series: [{
data: [crop_prop.b1_all_tif, crop_prop.b2_all_tif,crop_prop.b3_all_tif,crop_prop.b4_all_tif
,crop_prop.b5_all_tif,crop_prop.b6_all_tif,crop_prop.b7_all_tif],
type: 'line'
}]
};
myChart.setOption(option);
})
}
NDVI = () =>{
const {crop_prop } = this.state
this.setState({
showChart:true
},()=>{
var myChart = echarts.init(document.getElementById('chart'));
var option = {
title: {
text: 'NDVI'
},
tooltip: {},
grid: {
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['3月12', '4月11', '5月26','6月20','7月10','8月24','9月3','10月18']
},
yAxis: {
type: 'value'
},
series: [{
data: [crop_prop.b1_ndvi202 ,crop_prop.b2_ndvi202 ,crop_prop.b3_ndvi202 ,crop_prop.b4_ndvi202
,crop_prop.b5_ndvi202 ,crop_prop.b6_ndvi202 ,crop_prop.b7_ndvi202 ,crop_prop.b8_ndvi202],
type: 'line'
}]
};
myChart.setOption(option);
})
}
render() {
const { map, chart,crop_prop,showChart } = this.state
return (
<div style={{ width: '100%', paddingTop: '80px' }}>
<div className={styles.layer}>
<Tree
checkable
defaultExpandedKeys={['wushu','crops']}
defaultSelectedKeys={['crops']}
defaultCheckedKeys={['crops']}
// onSelect={this.onSelect}
onCheck={this.onCheck}
>
<TreeNode title="乌苏区位图" key="wushu">
<TreeNode title="影像" key="image"></TreeNode>
<TreeNode title="边界" key="boundary"></TreeNode>
</TreeNode>
<TreeNode title="全部农作物" key="crops">
<TreeNode title="番茄" key="fanqie"></TreeNode>
<TreeNode title="棉花" key="mianhua"></TreeNode>
<TreeNode title="蔬菜" key="shucai"></TreeNode>
<TreeNode title="小麦" key="xiaomai"></TreeNode>
<TreeNode title="玉米" key="yumi"></TreeNode>
<TreeNode title="其他" key="qita"></TreeNode>
</TreeNode>
</Tree>
<Divider orientation="right"><a onClick={this.tongji}>统计面积</a></Divider>
<div className={styles.desc}>
{ crop_prop && (
<>
{/* <div className={styles.title}>农作物详情</div> */}
<Divider orientation="left">农作物详情</Divider>
<div className={styles.item}><label>乡镇:</label><span>{crop_prop.xiangzhen}</span></div>
<div className={styles.item}><label>土地:</label><span>{crop_prop.classname}</span></div>
<div className={styles.item}><label>类型:</label><span>{crop_prop.kind}</span></div>
<div className={styles.item}><label>面积:</label><span>{crop_prop.Shape_Area}</span></div>
<div className={styles.item}><label>编码:</label><span>{crop_prop.code}</span></div>
<Divider orientation="right"><a onClick={this.zhangshi}>查看长势</a></Divider>
<Divider orientation="right"><a onClick={this.NDVI}>查看NDVI</a></Divider>
</>
)
}
</div>
{
showChart && (
<div className={styles.chart}>
<div id="chart" ref="chart" style={{ width: '100%',height:'100%' }}></div>
</div>
)
}
</div>
<div className={styles.map} id="map">
<div className={styles.toolbar}>
<div className={styles.bar} onClick={this.location}>
<div id="allmap" style={{display:none}}></div>
<p><Icon type="environment" /></p>
</div>
<div className={styles.bar} onClick={this.anhei}>
<p>暗黑</p>
</div>
<div className={styles.bar} onClick={this.luwang}>
<p>路网</p>
</div>
<div className={styles.bar} onClick={this.weixing}>
<p>卫星</p>
</div>
</div>
</div>
</div>
)
}
}
var Drag = /*@__PURE__*/(function (PointerInteraction) {
function Drag() {
PointerInteraction.call(this, {
handleDownEvent: handleDownEvent,
handleDragEvent: handleDragEvent,
handleMoveEvent: handleMoveEvent,
handleUpEvent: handleUpEvent
});
/**
* @type {import("../src/ol/coordinate.js").Coordinate}
* @private
*/
this.coordinate_ = null;
/**
* @type {string|undefined}
* @private
*/
this.cursor_ = 'pointer';
/**
* @type {Feature}
* @private
*/
this.feature_ = null;
/**
* @type {string|undefined}
* @private
*/
this.previousCursor_ = undefined;
}
if (PointerInteraction) Drag.__proto__ = PointerInteraction;
Drag.prototype = Object.create(PointerInteraction && PointerInteraction.prototype);
Drag.prototype.constructor = Drag;
return Drag;
}(PointerInteraction));
/**
* @param {import("../src/ol/MapBrowserEvent.js").default} evt Map browser event.
* @return {boolean} `true` to start the drag sequence.
*/
function handleDownEvent(evt) {
var map = evt.map;
var feature = map.forEachFeatureAtPixel(evt.pixel,
function (feature) {
return feature;
});
if (feature) {
this.coordinate_ = evt.coordinate;
this.feature_ = feature;
}
return !!feature;
}
/**
* @param {import("../src/ol/MapBrowserEvent.js").default} evt Map browser event.
*/
function handleDragEvent(evt) {
var deltaX = evt.coordinate[0] - this.coordinate_[0];
var deltaY = evt.coordinate[1] - this.coordinate_[1];
var geometry = this.feature_.getGeometry();
geometry.translate(deltaX, deltaY);
this.coordinate_[0] = evt.coordinate[0];
this.coordinate_[1] = evt.coordinate[1];
}
/**
* @param {import("../src/ol/MapBrowserEvent.js").default} evt Event.
*/
function handleMoveEvent(evt) {
if (this.cursor_) {
var map = evt.map;
var feature = map.forEachFeatureAtPixel(evt.pixel,
function (feature) {
return feature;
});
var element = evt.map.getTargetElement();
if (feature) {
if (element.style.cursor != this.cursor_) {
this.previousCursor_ = element.style.cursor;
element.style.cursor = this.cursor_;
}
} else if (this.previousCursor_ !== undefined) {
element.style.cursor = this.previousCursor_;
this.previousCursor_ = undefined;
}
}
}
/**
* @return {boolean} `false` to stop the drag sequence.
*/
function handleUpEvent() {
this.coordinate_ = null;
this.feature_ = null;
return false;
}
export default Nongye
后端主要采用django框架写的接口
后端 核心代码
功能界面
说明:
左侧目录树对图层进行隐藏,影像/边界是单独图层,农作物整是一个图层,分了不同类别,这里有区别;
统计面积是统计上面农作物分类的面积,这里统计一次后写入indexdb,方便下次快速读取
点击地块左侧显示地块的详细信息,包括可查看长势图和nvdi图
如有兴趣, 能帮到您,联系我 qq:851356263 ,请说明来意,谢谢!