漫漫技术路

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

使用React+Three.js 封装一个三维地球

发表于 2017-09-23 | 更新于 2018-11-30 | 分类于 前端技术

良久没有写过博客了,最近忙的焦头烂额,忽略了博客,罪过罪过。今天补充一篇,前一段时间研究过的技术,使用React+Three.js 封装一个三维地球,支持鼠标的交互行为。其实也实现了对有坐标的json数据展示在地球上的功能,以后会有补充。

github仓库地址:

https://github.com/zrysmt/react-threejs-app

整体做完之后的效果图:

废话少说,直接上环境

1.环境

使用facebook给出的脚手架工具create-react-app.

1
2
3
4
npm install -g create-react-app

create-react-app react-threejs-app
cd react-threejs-app/

执行

1
npm start

浏览器会自动打开localhost:3000。

2.背景知识

Three.js简单来说就是封装了WebGL一些易用的API接口,我们知道只使用WebGL比较低效。具体的关于WebGL的技术给出两篇博客的入口,关于Three.js可以参考文章最后给出的参考阅读部分。

  • WebGL基础简明教程1-简介
  • WebGL基础简明教程2-基础知识

如果不是很了解WebGL技术也没有关系,不妨现在先看看Three.js创建模型的整体过程。

安装需要的库,three是Three.js的库,three-orbitcontrols用来支持鼠标的交互行为的库。

1
npm i three three-orbitcontrols --save

3.一步一个脚印

3.1 准备一张高清的世界地图

这里在github仓库中已经给出。

3.2 定义一个组件ThreeMap

在ThreeMap.js定义组件ThreeMap,并且创建改组件的样式ThreeMap.css。css定义三维地球的容器的宽度和高度。

1
2
3
4
#WebGL-output{
width: 100%;
height: 700px;
}

并且该组件在App.js引用。

3.3 引入库和样式

1
2
3
4
5
import './ThreeMap.css';
import React, { Component } from 'react';
import * as THREE from 'three';
import Orbitcontrols from 'three-orbitcontrols';
import Stats from './common/threejslibs/stats.min.js';

3.4 初始化方法入口和要渲染的虚拟DOM

1
2
3
componentDidMount(){
this.initThree();
}

要渲染的虚拟DOM设定好

1
2
3
4
5
render(){
return(
<div id='WebGL-output'></div>
)
}

3.4 initThree方法

  • 创建场景
1
2
let scene;
scene = new THREE.Scene();
  • 创建Group
1
2
3
let group;
group = new THREE.Group();
scene.add( group );
  • 创建相机
1
2
3
4
5
camera = new THREE.PerspectiveCamera( 60, width / height, 1, 2000 );
camera.position.x = -10;
camera.position.y = 15;
camera.position.z = 500;
camera.lookAt( scene.position );
  • 相机作为Orbitcontrols的参数,支持鼠标交互
1
2
let orbitControls = new Orbitcontrols(camera);
orbitControls.autoRotate = false;
  • 添加光源:环境光和点光源
1
2
3
4
5
6
let ambi = new THREE.AmbientLight(0x686868); //环境光
scene.add(ambi);
let spotLight = new THREE.DirectionalLight(0xffffff); //点光源
spotLight.position.set(550, 100, 550);
spotLight.intensity = 0.6;
scene.add(spotLight);
  • 创建模型和材质
1
2
3
4
5
6
7
8
9
let loader = new THREE.TextureLoader();
let planetTexture = require("./assets/imgs/planets/Earth.png");

loader.load( planetTexture, function ( texture ) {
let geometry = new THREE.SphereGeometry( 200, 20, 20 );
let material = new THREE.MeshBasicMaterial( { map: texture, overdraw: 0.5 } );
let mesh = new THREE.Mesh( geometry, material );
group.add( mesh );
} );
  • 渲染
1
2
3
4
5
6
let renderer;
renderer = new THREE.WebGLRenderer();
renderer.setClearColor( 0xffffff );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( width, height );
container.appendChild( renderer.domElement );
  • 增加监控的信息状态
1
2
stats = new Stats();
container.appendChild( stats.dom );

将以上封装到init函数中

  • 动态渲染,地球自转
1
2
3
4
5
6
7
8
9
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
group.rotation.y -= 0.005; //这行可以控制地球自转
renderer.render( scene, camera );
}

调用的顺序是:

1
2
init();
animate();

大功告成,一个可交互的三维地球就可以使用了。

4.ThreeMap.js整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import './ThreeMap.css';
import React, { Component } from 'react';
import * as THREE from 'three';
import Orbitcontrols from 'three-orbitcontrols';
import Stats from './common/threejslibs/stats.min.js';

class ThreeMap extends Component{
componentDidMount(){
this.initThree();
}
initThree(){
let stats;
let camera, scene, renderer;
let group;
let container = document.getElementById('WebGL-output');
let width = container.clientWidth,height = container.clientHeight;

init();
animate();

function init() {
scene = new THREE.Scene();
group = new THREE.Group();
scene.add( group );

camera = new THREE.PerspectiveCamera( 60, width / height, 1, 2000 );
camera.position.x = -10;
camera.position.y = 15;
camera.position.z = 500;
camera.lookAt( scene.position );

//控制地球
let orbitControls = new /*THREE.OrbitControls*/Orbitcontrols(camera);
orbitControls.autoRotate = false;
// let clock = new THREE.Clock();
//光源
let ambi = new THREE.AmbientLight(0x686868);
scene.add(ambi);

let spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(550, 100, 550);
spotLight.intensity = 0.6;

scene.add(spotLight);
// Texture
let loader = new THREE.TextureLoader();
let planetTexture = require("./assets/imgs/planets/Earth.png");

loader.load( planetTexture, function ( texture ) {
let geometry = new THREE.SphereGeometry( 200, 20, 20 );
let material = new THREE.MeshBasicMaterial( { map: texture, overdraw: 0.5 } );
let mesh = new THREE.Mesh( geometry, material );
group.add( mesh );
} );

renderer = new THREE.WebGLRenderer();
renderer.setClearColor( 0xffffff );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( width, height );
container.appendChild( renderer.domElement );
stats = new Stats();
container.appendChild( stats.dom ); //增加状态信息

}

function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
group.rotation.y -= 0.005; //这行可以控制地球自转
renderer.render( scene, camera );
}
}
render(){
return(
<div id='WebGL-output'></div>
)
}
}

export default ThreeMap;

参考阅读:

  • WebGL基础简明教程1-简介
  • WebGL基础简明教程2-基础知识
  • 图解WebGL&Three.js工作原理

基于WebGL的大数据二三维可视化--uber的deck.gl介绍

发表于 2017-06-02 | 更新于 2018-11-30 | 分类于 前端技术

deck.gl是由uber开发并开源出来的基于WebGL的大数据量可视化框架。它具有提供不同类型可视化图层,GPU渲染的高性能,React和Mapbox GL集成,结合地理信息数据(GPS)的特点。下面我们就举两个例子探索一下这个神奇的库。

1.第一站:将源码的例子跑起来

源码在github里,首先克隆出来。

npm 下载,在项目根目录下

1
npm install

(友情提醒,如果npm下载速度比较慢,可以使用淘宝镜像,详细请自行搜索)

下面我们来到例子中,和上不一样在example文件夹下下载安装

1
npm install

安装成功之后,要申请mapbox的一个key。方法如下:
在mapbox网站注册后进入https://www.mapbox.com/studio/account/tokens/,我们便可以在Default Public Token
找到我们需要的mapbox key。

在例子文件夹下命令行下输入

1
export MAPBOX_ACCESS_TOKEN=<上面申请到的key>

注意:cmd会不认export,可以使用git命令行,或者直接在代码中加入

1
2
//const MAPBOX_TOKEN = process.env.MAPBOX_ACCESS_TOKEN; //改为
const MAPBOX_TOKEN = <上面申请到的key>

然后执行

1
npm run start

浏览器会自动打开,地址为localhost:3030。

第一例子3d-heatmap,我们看到
对应官网上的例子http://uber.github.io/deck.gl/#/examples/core-layers/hexagon-layer

这是一个HexagonLayer的例子。
第二个例子是利用geojson
对应官网上的例子http://uber.github.io/deck.gl/#/examples/core-layers/geojson-layer

2.第二站:先拿一个例子看看

第一例子3d-heatmap,显示效果上面有给出。

源码路径。

https://github.com/uber/deck.gl/tree/master/examples/3d-heatmap

deck.gl 是基于地理信息数据的,所以可视化很多都会分层两层,一层是地图数据底图,一层是可视化的数据。这个观念我们要记住,带进去看整个例子。对于这个例子组件MapGL是地里数据底图,组件DeckGLOverlay是可视化成的数据。

基本结构很见简单。

由于是使用React框架搭建,html很简单

  • index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!doctype html>
<html>
<head>
<meta charset='UTF-8' />
<title>deck.gl example</title>
<style>
body {margin: 0; padding: 0; overflow: hidden;}
</style>
</head>
<body>
<script src='bundle.js'></script>
</body>
</html>
  • deckgl-overlay.js

首先引入React和deckgl。

1
2
import React, {Component} from 'react';
import DeckGL, {HexagonLayer} from 'deck.gl';

几个变量先放出来,光线设置(LIGHT_SETTINGS),颜色范围设置(colorRange),高度显示范围(elevationScale),一些默认的属性(defaultProps)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const LIGHT_SETTINGS = {
lightsPosition: [-0.144528, 49.739968, 8000, -3.807751, 54.104682, 8000],
ambientRatio: 0.4,
diffuseRatio: 0.6,
specularRatio: 0.2,
lightsStrength: [0.8, 0.0, 0.8, 0.0],
numberOfLights: 2
};

const colorRange = [
[1, 152, 189],
[73, 227, 206],
[216, 254, 181],
[254, 237, 177],
[254, 173, 84],
[209, 55, 78]
];

const elevationScale = {min: 1, max: 50};

const defaultProps = {
radius: 1000,
upperPercentile: 100,
coverage: 1
};

我们建立叫DeckGLOverlay的组件

1
2
export default class DeckGLOverlay extends Component {
}

组件中提供了两个静态的属性,静态属性可以使用[类名].[方法名]调用,例如:DeckGLOverlay.defaultColorRange。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static get defaultColorRange() {//默认的颜色范围
return colorRange;
}

static get defaultViewport() {//默认的视图
return {
longitude: -1.4157267858730052,
latitude: 52.232395363869415,
zoom: 6.6,
minZoom: 5,
maxZoom: 15,
pitch: 40.5,
bearing: -27.396674584323023
};
}

constructor是构造函数,主要看的是_animateHeight对组件state的设置,这个是用来控制高度的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
constructor(props) {
super(props);
this.startAnimationTimer = null;
this.intervalTimer = null;
this.state = {
elevationScale: elevationScale.min
};

this._startAnimate = this._startAnimate.bind(this);
this._animateHeight = this._animateHeight.bind(this);

}

componentDidMount() {
this._animate();
}

componentWillReceiveProps(nextProps) {
if (nextProps.data.length !== this.props.data.length) {
this._animate();
}
}

componentWillUnmount() {
this._stopAnimate();
}

_animate() {
this._stopAnimate();

// wait 1.5 secs to start animation so that all data are loaded
this.startAnimationTimer = window.setTimeout(this._startAnimate, 1500);
}

_startAnimate() {
this.intervalTimer = window.setInterval(this._animateHeight, 20);
}

_stopAnimate() {
window.clearTimeout(this.startAnimationTimer);
window.clearTimeout(this.intervalTimer);
}

_animateHeight() {
if (this.state.elevationScale === elevationScale.max) {
this._stopAnimate();
} else {
this.setState({elevationScale: this.state.elevationScale + 1});
}
}

_initialize(gl) {
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
}

render渲染方法,layers是可视化渲染的图层定义变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
render() {
const {viewport, data, radius, coverage, upperPercentile} = this.props;

if (!data) {
return null;
}

const layers = [
new HexagonLayer({
id: 'heatmap',
colorRange,
coverage,
data,
elevationRange: [0, 3000],
elevationScale: this.state.elevationScale, //高度
extruded: true,
getPosition: d => d, //位置
lightSettings: LIGHT_SETTINGS,
onHover: this.props.onHover,
opacity: 1,
pickable: Boolean(this.props.onHover),
radius,
upperPercentile
})
];

return <DeckGL {...viewport} layers={layers} onWebGLInitialized={this._initialize} />;
}

  • app.js

引入:

1
2
3
4
5
6
import React, {Component} from 'react';
import {render} from 'react-dom';
import MapGL from 'react-map-gl';
import DeckGLOverlay from './deckgl-overlay.js';

import {csv as requestCsv} from 'd3-request';//加载csv文件

mapbox key:

1
const MAPBOX_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;

组件Root:
在构造函数中,我们将两个关键的变量viewport和data均放入到state中。这里的MapGL组件就是地理底图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Root extends Component {

constructor(props) {
super(props);
this.state = {
viewport: {
...DeckGLOverlay.defaultViewport,
width: 500,
height: 500
},
data: null
};
//请求csv数据
requestCsv('./data/heatmap-data.csv', (error, response) => {
if (!error) {
const data = response.map(d => ([Number(d.lng), Number(d.lat)]));
this.setState({data});
}
});
}

componentDidMount() {
window.addEventListener('resize', this._resize.bind(this));
this._resize();
}

_resize() {
this._onChangeViewport({
width: window.innerWidth,
height: window.innerHeight
});
}

_onChangeViewport(viewport) {
this.setState({
viewport: {...this.state.viewport, ...viewport}
});
}

render() {
const {viewport, data} = this.state;

return (
<MapGL
{...viewport}
mapStyle="mapbox://styles/mapbox/dark-v9"
perspectiveEnabled={true}
onChangeViewport={this._onChangeViewport.bind(this)}
mapboxApiAccessToken={MAPBOX_TOKEN}>
<DeckGLOverlay
viewport={viewport}
data={data || []}
/>
</MapGL>
);
}
}

渲染:

1
render(<Root />, document.body.appendChild(document.createElement('div')));

MapGL组件我们来修改下mapStyle来重新生成一张图。

1
mapStyle="mapbox://styles/mapbox/satellite-v9"

来看下显示效果,地图就变成了卫星影像。

mapbox提供了几款基础的样式,我们还可以自定义样式使用。

3.第三站:总结一下

通过上面我们就分析了一个简单的例子,使用Deck.gl,MapGL组件,利用两百多行代码就可以渲染14万多条数据,而且显示效果让我们惊叹,还支持交互,性能比较好,基于WebGL的大数据二三维可视化一个值得研究的方向。

参考阅读:

  • uber deck.gl官网
  • uber可能用到的可视化技术
  • Uber工程的deck.gl框架下的Web数据可视化集

leaflet可视化平台搭建

发表于 2017-05-22 | 更新于 2018-11-30 | 分类于 WebGIS

leaflet-viz–Leaflet可视化平台

https://github.com/zrysmt/leaflet-viz

leaflet是一个开源的前端地图交互类库,比较轻量级,支持移动端。而且有丰富的插件资源可供我们使用。

Echarts是百度开源的前端可视化类库,提供丰富的前端可视化图表,平台中重要的一部分是我们要将leaflet和Echarts结合在一起。

该平台是基于leaflet及其插件搭建的一个方便可用的可视化平台。详细参见[示例Demo]。(https://zrysmt.github.io/demo/leaflet-demo/)

1. 安装与编译

  • 安装
1
npm install
  • 编译

debug模式

1
npm run dev

输出

1
npm run build

2.示例简介

示例地址:https://zrysmt.github.io/demo/leaflet-demo/

示例包含最基本的GIS功能和可视化Demo

3.基础的GIS功能

  • 拖放、全图、定位、打印出图片、打印出PDF功能。
  • 地图缩放、比例尺显示功能
  • 测量面积和距离功能
  • 图层切换,提供丰富的图层切换
  • 地图搜索功能
  • 基础绘图功能

4.可视化示例

  • 热力图

引入:

1
import '../common/leaflet-plugin/HeatLayer.js';

使用:

1
2
3
4
5
var heat = L.heatLayer([
[50.5, 30.5, 0.2], // lat, lng, intensity
[50.6, 30.4, 0.5],
...
], {radius: 25}).addTo(map)

效果图:

  • 结合Echarts
1
2
3
4
5
6
7
8
9
10
let overlay = new L.echartsLayer3(map, echarts);
let chartsContainer = overlay.getEchartsContainer();
let myChart = overlay.initECharts(chartsContainer);
window.onresize = myChart.onresize;
console.log("chartsContainer:", chartsContainer);
if (type == "qianxi") {
overlay.setOption(ecOption);
} else if (type == "scatter") {
overlay.setOption(scatterOption);
}

迁徙图(选择了高德卫星底图)

散点图(选择了Geoq午夜蓝底图)

  • DivIcon结合Echarts

这个方案不太适合大数据量的渲染。
我写了两个简单的功能函数,一个用来渲染可视化图(echartsIcon),一个专门用来渲染图例(echartsLegend)。
效果图(底图选择了为google底图)

1
2
import echartsIcon from '../common/plugin/echartsIcon.js'; //echartsLegend
import echartsLegend from '../common/plugin/echartsLegend.js'; //echartsLegend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
let option = {
tooltip: {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
series: [{
name: '访问来源',
type: 'pie',
radius: '55%',
center: ['50%', '50%'],
label: {
normal: {
show: false
},
emphasis: {
show: false
}
},
lableLine: {
normal: {
show: false
},
emphasis: {
show: false
}
},
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
//经纬度不能相同
let latlngs = [
[30, 104],
[31, 110],
[34, 120]
];
option.datas = [
[
{ value: 335, name: '直接访问' },
{ value: 310, name: '邮件营销' },
{ value: 234, name: '联盟广告' },
{ value: 135, name: '视频广告' },
{ value: 1548, name: '搜索引擎' }
],
[
{ value: 345, name: '直接访问' },
{ value: 410, name: '邮件营销' },
{ value: 244, name: '联盟广告' },
{ value: 145, name: '视频广告' },
{ value: 548, name: '搜索引擎' }
],
[
{ value: 445, name: '直接访问' },
{ value: 410, name: '邮件营销' },
{ value: 244, name: '联盟广告' },
{ value: 145, name: '视频广告' },
{ value: 148, name: '搜索引擎' }
],
];
echartsIcon(map, latlngs, option);
//图例
let legendOption = {
orient: 'vertical',
left: 'left',
width: "90px",
height: "140px",
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
};
echartsLegend(map, legendOption);
  • leaflet-dvf

leaflet-dvf是一个基于leaflet的数据可视化框架,提供有比较多的可视化图形。

1
2
import "../common/leaflet-plugin/Leaflet.dvf/css/dvf.css";
import "leaflet-dvf";

地震图:

收入水平图:

需要的数据

1
2
import earthquakesData from '../data/earthquakes.json';
import countryData from '../data/countryData.js';

地震图代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
let lastLayer;

let eqfeed_callback = function(data) {

// Initialize framework linear functions for mapping earthquake data properties to Leaflet style properties
// Color scale - green to red using the basic HSLHueFunction
let magnitudeColorFunction = new L.HSLHueFunction(new L.Point(0, 90), new L.Point(10, 0), { outputSaturation: '100%', outputLuminosity: '25%', postProcess: null });
let magnitudeFillColorFunction = new L.HSLHueFunction(new L.Point(0, 90), new L.Point(10, 0), { outputSaturation: '100%', outputLuminosity: '50%', postProcess: null });
let magnitudeRadiusFunction = new L.LinearFunction(new L.Point(0, 10), new L.Point(10, 30), { postProcess: null });

let now = Math.round((new Date()).getTime());
let start = now - 86400000;

// Initialize a linear function to map earthquake time to opacity
let timeOpacityFunction = new L.LinearFunction(new L.Point(start, 0.3), new L.Point(now, 1));
let fontSizeFunction = new L.LinearFunction(new L.Point(0, 8), new L.Point(10, 24));

let textFunction = function(value) {
return {
text: value,
style: {
'font-size': fontSizeFunction.evaluate(value)
}
};
};

// Setup a new data layer
let dataLayer = new L.DataLayer(data, {
recordsField: 'features',
latitudeField: 'geometry.coordinates.1',
longitudeField: 'geometry.coordinates.0',
locationMode: L.LocationModes.LATLNG,
displayOptions: {
'properties.mag': {
displayName: '震级',
color: magnitudeColorFunction,
fillColor: magnitudeFillColorFunction,
radius: magnitudeRadiusFunction,
text: textFunction
},
'properties.time': {
displayName: '时间',
opacity: timeOpacityFunction,
fillOpacity: timeOpacityFunction,
displayText: function(value) {
return moment.unix(value / 1000).format('MM/DD/YY HH:mm');
}
}
},
layerOptions: {
numberOfSides: 4,
radius: 10,
weight: 1,
color: '#000',
opacity: 0.2,
stroke: true,
fillOpacity: 0.7,
dropShadow: true,
gradient: true
},
tooltipOptions: {
iconSize: new L.Point(90, 90), //hover框大小
iconAnchor: new L.Point(-4, 76)
},
onEachRecord: function(layer, record, location) {
let $html = $(L.HTMLUtils.buildTable(record));

layer.bindPopup($html.wrap('<div/>').parent().html(), {
minWidth: 400,
maxWidth: 400
});
}
});

// Add the data layer to the map
map.addLayer(dataLayer);

lastLayer = dataLayer;
};

this.mapSetting();

if (lastLayer) {
map.removeLayer(lastLayer);
}

console.log("earthquakesData:", earthquakesData);
eqfeed_callback(earthquakesData)

收入水平图代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
let incomeLevelTypes = ['OEC', 'NOC', 'UMC', 'MIC', 'LMC', 'LIC', 'HPC'];
let valueArray = [{ "id": "HIC", "value": "高收入(HIC)" },
{ "id": "HPC", "value": "收入严重不足(HIPC)" }, { "id": "INX", "value": "未分类(INX)" },
{ "id": "LIC", "value": "低收入(LIC)" }, { "id": "LMC", "value": "中等偏下(LMC)" },
{ "id": "LMY", "value": "中低等收入" }, { "id": "MIC", "value": "中等收入(MIC)" },
{ "id": "NOC", "value": "高收入:nonOECD(NOC)" }, { "id": "OEC", "value": "高收入: OECD(OEC)" },
{ "id": "UMC", "value": "中等偏上(UMC)" }
];
let getMap = function(valueArray) {
let map = {};
for (let index = 0; index < valueArray.length; ++index) {
let value = valueArray[index];

map[value['id']] = value['value'];
}
return map;
};

let valueMap = getMap(valueArray);

let incomeLevelToText = function(value) {
return valueMap[incomeLevelTypes[value]];
};

let colorFunction1 = new L.HSLLuminosityFunction(new L.Point(0, 0.2), new L.Point(incomeLevelTypes.length - 1, 0.75), { outputHue: 0, outputLuminosity: '100%' });
let fillColorFunction1 = new L.HSLLuminosityFunction(new L.Point(0, 0.5), new L.Point(incomeLevelTypes.length - 1, 1), { outputHue: 0, outputLuminosity: '100%' });

let styles = new L.StylesBuilder(incomeLevelTypes, {
displayName: incomeLevelToText,
color: colorFunction1,
fillColor: fillColorFunction1
});

let options = {
recordsField: '1',
locationMode: L.LocationModes.COUNTRY,
codeField: 'id',
displayOptions: {
'incomeLevel.id': {
displayName: '收入水平',
styles: styles.getStyles()
}
},
layerOptions: {
fillOpacity: 0.7,
opacity: 1,
weight: 1
},
tooltipOptions: {
iconSize: new L.Point(100, 65),
iconAnchor: new L.Point(-5, 65)
},

onEachRecord: function(layer, record) {
let $html = $(L.HTMLUtils.buildTable(record));

layer.bindPopup($html.wrap('<div/>').parent().html(), {
maxWidth: 400,
minWidth: 400
});
}
};

let incomeLayer = new L.ChoroplethDataLayer(incomeLevels, options);

let legendControl = new L.Control.Legend();

legendControl.addTo(map);
map.addLayer(incomeLayer);

5.GeoJSON

假设我们需要自己做个地图,然后加载到平台中。这个时候可以使用我们提供了编辑栏,编辑后会得到坐标,把坐标转成GeoJSON数据存到数据库中,下次加载的时候解析即可。
还可以使用GIS工具,做成GeoJSON的数据,然后使用下面的方法加载,详细说明的传送门在这,当然这包括MapServer的东西,我们将在第六部分有简要说明。
示例图:

引入数据:

1
import { json_china } from '../data/china.js';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function pop_0(feature, layer) {//部分有省略
let popupContent = '<table>\
<tr>\
<td colspan="2">面积:' + (feature.properties['AREA'] !== null ? Autolinker.link(String(feature.properties['AREA'])) : '') + '</td>\
</tr>\
<tr>\
<td colspan="2">周长:' + (feature.properties['PERIMETER'] !== null ? Autolinker.link(String(feature.properties['PERIMETER'])) : '') + '</td>\
</tr>\
<tr>\
<td colspan="2">名称:' + (feature.properties['NAME'] !== null ? Autolinker.link(String(feature.properties['NAME'])) : '') + '</td>\
</tr>\
</table>';
layer.bindPopup(popupContent);
}

function style_0() {
return {
pane: 'pane_0',
opacity: 1,
color: 'rgba(0,0,0,0.494117647059)',
dashArray: '',
lineCap: 'butt',
lineJoin: 'miter',
weight: 1.0,
fillOpacity: 1,
fillColor: 'rgba(64,98,210,0.494117647059)',
}
}
map.createPane('pane_0');
map.getPane('pane_0').style.zIndex = 400;
map.getPane('pane_0').style['mix-blend-mode'] = 'normal';
let layer_0 = new L.geoJson(json_china, {
attribution: '<a href=""></a>',
pane: 'pane_0',
onEachFeature: pop_0,
style: style_0
});

6.MapServer

再进一步,如果我们想要自己的地图服务器,这个时候就需要使用到了MapServer,具体的说明见传送门

有个示例在test文件夹下:leaflet-mapserver.html。注意这个是需要配置好服务器的,在这里直接预览是不能成功的。
示例得到我们的世界地图的地图服务器。

其实leaflet和D3等开源库结合起来,能得到很多丰富多彩的可视化图,姑且留下点神秘,这里就不再探究两者结合的神奇魅力了。

2017阿里实习校招-前端技术视频面试体会

发表于 2017-05-22 | 更新于 2018-11-30 | 分类于 前端技术

快要秋招找工作了,预约的暑期实习面试,也是为了秋招攒点经验。约的时间是15:15 - 16:00,焦急又必须有耐心的等待后,终于在下午四点左右接通了视频,紧张ing。

首先面试官介绍了自己的花名,心里紧张没记下来,现在想想挺后悔的。但是谁让我紧张了呢?
然后是我的自我介绍,简单的介绍了一下我的基本情况,把技能和项目都介绍了介绍。
面试官态度挺好的,平易近人的感觉,慢慢的也就不紧张了,后面面试官一路45度仰天扣鼻子,也是让我乐了,心态更加轻松随意了。

面试的前端工程师,当然最重要的技术问题。上来就是让我共享屏幕,然后打开IDE写个排序,尼玛宝宝好怕,虽然简单,但是全部写下来运行太痛苦了。

1.数组排序

  • 原生实现 sort函数,倒是很快就写出来了,然后随时就让我用不是原生的方法实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var arr = [1,6,8,34,20,10];
    function main(arr,sortType){
    return arr.sort(function(num1,num2){
    if(sortType == "desc"){
    return num1 < num2;
    }else{
    return num1 > num2;
    }
    })
    }
  • 快排 我记得模棱两可,没写出来啊,心痛啊。现在把正确的贴在这里。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function quickSort(arr) {
    if(arr.length<=1) {
    return arr;
    }

    let leftArr = [];
    let rightArr = [];
    let q = arr[0];
    for(let i = 1,l=arr.length; i<l; i++) {
    if(arr[i]>q) {
    rightArr.push(arr[i]);
    }else{
    leftArr.push(arr[i]);
    }
    }

    return [].concat(quickSort(leftArr),[q],quickSort(rightArr));
    }
  • 还好冒泡还记得,又用冒泡写的,马马虎虎能运行了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function bubbleSort(arr) {  
    for(let i = 0,l=arr.length;i<l-1;i++) {
    for(let j = i+1;j<l;j++) {
    if(arr[i]>arr[j]) {
    let tem = arr[i];
    arr[i] = arr[j];
    arr[j] = tem;
    }
    }
    }
    return arr;
    }

2.ES6

提了下ES6,问了ES6的let,const与var的区别,特意说下let和const有块级作用域。var没有。然后又问了我都有什么作用域,我说ES5没有块级作用域,有全局作用域和函数作用域。然后面试官补充了一个对象作用域,这个我知道只是好像没经过总结过啊。

问我在项目中使用的ES6都有哪些?我说Promoise挺优雅的,然后让我写写,然后我就写了个XmlHttpServer,啊悲了个催了的,我的编辑器没有智能提示(论一个智能提醒的IDE有多重要),然后我手写的ajax请求,忘记了很多细节,怎么写啊,写了部分,面试官看不下去了吧(心伤),说他明白我的意思,你不用全部写下来,把过程大概写好就行了。然后我就写了怎么调用,我是理解了Promise,不知道这个问题面试官给打多少分,所以说坑都是自己挖的。

其实介绍了几次我使用了React+react router,估计阿里内部使用的是Weex,Kissy这样的自己的框架,面试官估计没有太接触React,所以一直没问我这方面的问题。

3.项目

一直不提我使用React写的项目,提起来我之前做的一个项目,那个项目我也没啥说的吧,我只能说我比较早做的,使用的都是老技术。运用的是Bootstrap,查询渲染数据,巴拉巴拉,感觉我说完的意思是我的这个项目不咋样,自己不相信自己了啊,怀疑了人生。

然后问我做了其他的有什么最深的体会。
所以我立刻就说了使用 React的项目(终于可以介绍了),然后我就说主要分了两个模块,一个是基础的显示模块,一个是地图模块,使用Openlayers api的组件化,还和github上的有个OpenLayer封装的做了个对比,我说有一个开源的使用React封装的Openlayers不方便,而且源码不全,我就自己封装了,面试官再次表现出了不太了解React,所以就没有继续追问下去。

4.对可视化的理解

估计看了我的博客,我的博客最近写的是WebGL,就问了我对WebGL的理解。
理解,这怎么说啊,好难过,我也不知道怎么描述啊。而且我也只是刚看了一个星期的书,还有一个周的Threejs罢了,我就说性能好,但是WebGL书写比较麻烦,所以大家都用Three.js,我也看了部分的Three.js。然后又顺嘴提了下我后面的研究方向, 目前还没有太深的理解(是不是这句话不应该说啊,应该忽悠忽悠的啊),然后就又随口说了下cesium.js,与我们的专业还有点关系–webgis,然后我们会在此基础上扩展。

总之问我理解,我又给出的印象是我的理解不深,痛心了。
然后给了个更大的题目,对可视化的理解,这题目呢,怎么说啊。我只能说大数据可视化和canvas,然后举了举例子,ECharts,datav,mapv等,然后说我对这方面挺有兴趣的,忘了面试官说什么了,好像的意思也只是你对这感兴趣,有个方向,没有深耕下去。

5.总结

这可以说是我第一次真正意义上的面试,第一次视频面试,还是阿里的。总之感觉是这次我没戏了,哪怕是个实习面试。看来是要多刷刷面试经验的。

离远方还是有很多路要走啊,还有两三个月时间,继续加油吧,一定要告诫自己不要气馁,坚持就是胜利。

简单体会,写至此处,感怀万千。,坚持就是胜利。 简单体会,写至此处,感怀万千。

WebGL基础简明教程2-基础知识

发表于 2017-05-17 | 更新于 2018-11-30 | 分类于 前端技术

上一篇我们介绍了使用WebGL的基础,包括顶点着色器、片元着色器、初始化WebGL,初始化着色器以及变换、动画、颜色、纹理等,这一部分的内容我们就来进入三维的世界。和上一篇文章一样,我们的这篇只做个大概的介绍,详细的内容部分请参阅《WebGL编程指南》一书。

代码存储在我的GitHub中。

https://github.com/zrysmt/data-viz/tree/master/webgl/demo

首先我们来绘制一个三维的实例。
示例程序:https://zrysmt.github.io/demo/webgl-demo/demo/10-HelloCube.html.

实例的源码程序:https://github.com/zrysmt/data-viz/blob/master/webgl/demo/HelloCube.js
其实这里我们应该注意到示例和源码的结构对应,下面的一些我可能只给出一个url地址。

1.视图、投影和索引矩阵

从main函数开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');

// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}

// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}

// Set the vertex coordinates and color
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the vertex information');
return;
}

// Set clear color and enable hidden surface removal
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);

// Get the storage location of u_MvpMatrix
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
if (!u_MvpMatrix) {
console.log('Failed to get the storage location of u_MvpMatrix');
return;
}

// Set the eye point and the viewing volume
var mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(30, 1, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);

// Pass the model view projection matrix to u_MvpMatrix
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

// Clear color and depth buffer
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// Draw the cube
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

顶点着色器和片元着色器

和上一篇有一些的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'uniform mat4 u_MvpMatrix;\n' + //模型矩阵,设置视图/投影
'varying vec4 v_Color;\n' + //传值给片元着色器
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' v_Color = a_Color;\n' +
'}\n';

// Fragment shader program
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' + //精度限定 中等精度
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';

关于attribute uniform varying
attribute:只能是全局的,只能出现在顶点着色器,标识逐顶点信息;
uniform:只能是全局的,可以在顶点着色器和片元着色器上,如果两个地方均定义,那么这变量被两个着色器共享了;
varying:只能是全局的,负责从顶点着色器向片元着色器传输数据。

视图矩阵(view matrix)

视点、观察点和上方向决定视图矩阵

1
2
var mvpMatrix = new Matrix4();
mvpMatrix.setLookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)

投影矩阵

投影的作用就是使得距离近的看的比较大,距离远的看的比较小
示例

1
projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);


三角形与可视空间的位置

其实投影矩阵的核心就是两种变换,1)按比例缩放;2)平移

消除被遮挡的面

1
gl.enable(gl.DEPTH_TEST);

清除颜色和深度buffer

1
2
// Clear color and depth buffer 
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

重要的公式

我们总结下一个重要的公式,WebGL绘制物体的位置为:

1
<投影矩阵> X <视图矩阵> X  <模型矩阵>  X  <顶点坐标>

绘制立方体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
// Vertex coordinates and color
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0 White
-1.0, 1.0, 1.0, 1.0, 0.0, 1.0, // v1 Magenta
-1.0, -1.0, 1.0, 1.0, 0.0, 0.0, // v2 Red
1.0, -1.0, 1.0, 1.0, 1.0, 0.0, // v3 Yellow
1.0, -1.0, -1.0, 0.0, 1.0, 0.0, // v4 Green
1.0, 1.0, -1.0, 0.0, 1.0, 1.0, // v5 Cyan
-1.0, 1.0, -1.0, 0.0, 0.0, 1.0, // v6 Blue
-1.0, -1.0, -1.0, 0.0, 0.0, 0.0 // v7 Black
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
0, 3, 4, 0, 4, 5, // right
0, 5, 6, 0, 6, 1, // up
1, 6, 7, 1, 7, 2, // left
7, 4, 3, 7, 3, 2, // down
4, 7, 6, 4, 6, 5 // back
]);

// Create a buffer object
var vertexColorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
if (!vertexColorBuffer || !indexBuffer) {
return -1;
}

// Write the vertex coordinates and color to the buffer object
// 存入缓冲:颜色/位置
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

var FSIZE = verticesColors.BYTES_PER_ELEMENT;
// Assign the buffer object to a_Position and enable the assignment
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
// Assign the buffer object to a_Color and enable the assignment
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if (a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);

// Write the indices to the buffer object
// 存入缓冲:索引
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

立方体的结构

1
2
3
4
5
6
7
  v6----- v5
/| /|
v1------v0|
| | | |
| |v7---|-|v4
|/ |/
v2------v3

首先顶点坐标和顶点坐标的颜色存储在verticesColors矩阵中。
而WebGL的绘制是按照三角形的形式一个一个绘制的,那么意味着一个立方体的一个面有两个三角形,一个立方体总共需要26 = 12个三角形,一个三角形需要3个顶点,那么一共需要绘制 312 = 36个顶点。我们知道一个立方体只需要8个顶点就可以了。为此我们在这里使用了索引矩阵(indices),索引矩阵也要写入缓存中。

这样我们就能完成绘制了

1
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);

注意这里的n = indices.length。

2.光照

我们主要关注下面三种形式的光照:

物理表面反射光线:分为漫反射和环境反射

环境反射

1
<环境反射光颜色> = <入射光颜色> X <表面基底色>

平行光下的漫反射

1
<漫反射光颜色> = <入射光颜色> X <表面基底色> X cosθ

即:

1
<漫反射光颜色> = <入射光颜色> X <表面基底色> X (<光线方向> · <法线方向>)

我们只用漫反射的效果的时候
示例

由于只考虑漫反射,右边的部分几乎是黑色了,这时候我们来考虑下环境光。
环境反射下的表面的反射光颜色

1
<表面的反射光颜色> = <漫反射颜色> + <环境反射光颜色>

示例

我们来看下着色器源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' + //表面颜色
'attribute vec4 a_Normal;\n' + // 表面法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色
'uniform vec3 u_LightDirection;\n' + // 漫反射入射光方向 (in the world coordinate, normalized)归一化后
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 归一化方向量
' vec3 normal = normalize(a_Normal.xyz);\n' +
// <归一化的光线方向> 点乘 <归一化法线向量>
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
// 计算漫反射颜色
' vec3 diffuse = u_DiffuseLight * a_Color.rgb * nDotL;\n' +
// 计算环境光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 两者相加得到物体最终的颜色
' v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
'}\n';

// Fragment shader program
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';

对于变化后的物体,物体的法向量也会改变
示例

1
<变换后的法向量> = <法向量> X <变化矩阵的逆转置矩阵>

1
2
3
normalMatrix.setInverseOf(modelMatrix); 
normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

在顶点着色器中变化原法向量

1
vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal))

最后我们来看下最终的漫反射光颜色的公式

1
<漫反射光颜色> = <入射光颜色> X <表面基底色> X (<光线方向> ·(<模型矩阵逆转置矩阵> X <法线方向>))

点光源

1
光线方向 = 归一化(点光源方向 - 顶点坐标)
1
2
// 顶点处的光线方向 = 点光源的光坐标 - 顶点坐标 
' vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n'

3.层次模型

有的物体的运动会带动别的物体,如上臂的运动会带动下臂和手掌的运动。

示例:https://zrysmt.github.io/demo/webgl-demo/ch09/JointModel.html
示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch09/JointModel.js


下边的(Arm1)会带动上边的(Arm2)运动,而上边的运动不会带动下边的。

思路:共用一个变换的模型矩阵:g_modelMatrix,Arm1变化g_modelMatrix也会变化,那么Arm2也会使用这个模型。谁能控制谁,关键是谁先写上去,谁后写上去,先写上去的控制后写上去的。

1
2
3
4
5
6
7
8
9
10
// Arm1 
var arm1Length = 10.0; // Length of arm1
g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw |
// Arm2
g_modelMatrix.translate(0.0, arm1Length, 0.0);    // Move to joint1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // Rotate around the z-axis
g_modelMatrix.scale(1.3, 1.0, 1.3); // Make it a little thicker
drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

4.几个高级功能的实例

  • 鼠标控制物体的旋转
  • 是否选中物体
  • HUD,结合canvas绘制文本
  • 雾化
  • 绘制阴影
  • 加载三维obj文件

WebGL基础简明教程1-简介

发表于 2017-05-17 | 更新于 2018-11-30 | 分类于 前端技术

我也是个初学WebGL的人,这部分的内容是我在看完《WebGL编程指南》一书后的精简教程。看完之后我对三维世界重建了一些观念,这篇文章是尽量在有限的内容中,一下介绍几个重要的基本的概念,后面我会分几篇再详细介绍几个重要的概念。

WebGL是利用HTML5的canvas绘制和渲染三维图形,再现代的浏览器中均支持。WebGL是从OpenGL ES中继承过来的。

代码存储在我的GitHub中。

https://github.com/zrysmt/data-viz/tree/master/webgl/demo

首先我们来绘制一个二维的实例,点击的时候绘制一个点。
示例程序:https://zrysmt.github.io/demo/webgl-demo/demo/0-simple.html.

1.WebGL二维:一次绘制一个点

html片段

注意WebGL canvas的坐标(右,z轴垂直屏幕向外)和二维canvas(左)不一样。

html片段很简单,我们使用就是canvas元素。

1
2
3
4
5
<body onload="main()">
<canvas id="webgl" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
</body>

WebGL执行流程

从main函数开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function main() {
var canvas = document.getElementById('webgl');
//初始化WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// 获取a_Position的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
// 注册鼠标点击事件
canvas.onmousedown = function(ev) {
click(ev, gl, canvas, a_Position);
};

// 设置<canvas>背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清除<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
}

顶点着色器

用来描述顶点的特性(如位置、颜色等)

1
2
3
4
5
6
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';

是一种类似C的语言。a_Position是一个attribute变量,vec4表示有四个浮点数组成的矢量。

片元着色器

进行逐片处理的过程如光照

1
2
3
4
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';

使用顶点着色器和片元着色器

初始化着色器

这部分的代码也是通用的,流程如下,具体代码我们在我的github中查看

1
2
3
4
5
6
7
* 1.创建着色器对象(gl.createShader())
* 2.向着色器对象中填充着色器程序的源代码(gl.shaderSource())
* 3.编译着色器(gl.compileShader())
* 4.创建程序对象(gl.createProgram())
* 5.为程序对象分配着色器(gl.attachShader())
* 6.连接程序对象(gl.linkProgram())
* 7.使用程序对象(gl.useProgram())

注册鼠标点击事件

注册鼠标事件,然后对坐标进行处理

1
2
3
canvas.onmousedown = function(ev) {
click(ev, gl, canvas, a_Position);
};

注意要转化为WebGL的坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function click(ev, gl, canvas, a_Position) {
var x = ev.clientX; // 鼠标的x坐标
var y = ev.clientY; // 鼠标的y坐标
var rect = ev.target.getBoundingClientRect();

x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2); //处理后相得canvas的x坐标
y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2); //处理后相得canvas的y坐标
g_points.push(x);
g_points.push(y);

gl.clear(gl.COLOR_BUFFER_BIT);

var len = g_points.length;
for (var i = 0; i < len; i += 2) {
// 将顶点位置传给attribute变量a_Position
gl.vertexAttrib3f(a_Position, g_points[i], g_points[i + 1], 0.0);
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
}
}

绘制

1
gl.drawArrays(gl.POINTS, 0, 1);


指定第一个参数可以绘制线或三角形,具体的这几个有什么意义,可以查看github中示例的源码。

2.WebGL二维:绘制多个点

一次性的将全部的点传给顶点着色器,这个时候就需要用到了缓冲对象。
我们以绘制个三角形为例(一次性至少传入三个点),示例程序:https://zrysmt.github.io/demo/webgl-demo/ch03/HelloTriangle.html。
效果如下所示:

对应的源码位置:https://github.com/zrysmt/data-viz/blob/master/webgl/ch03/HelloTriangle.js

创建缓冲区对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3; // The number of vertices

// 创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}

// 绑定
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
// 缓冲区对象传给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 启用
gl.enableVertexAttribArray(a_Position);
//顶点个数
return n;
}

3.WebGL二维-变换与动画

变换

学过线性代数的都知道矢量的变化是可以通过矩阵(4 X 4的,可以容纳下三种变化)完成的。

我们来看顶点着色器代码

1
2
3
4
5
6
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_xformMatrix;\n' +
'void main() {\n' +
' gl_Position = u_xformMatrix * a_Position;\n' +
'}\n';

u_xformMatrix就是变化的矩阵。
旋转示例:https://zrysmt.github.io/demo/webgl-demo/ch03/RotatedTriangle_Matrix.html
旋转示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch03/RotatedTriangle_Matrix.js
缩放示例:https://zrysmt.github.io/demo/webgl-demo/ch03/ScaledTriangle_Matrix.html
缩放示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch03/ScaledTriangle_Matrix.js
平移示例:https://zrysmt.github.io/demo/webgl-demo/ch03/TranslatedTriangle.html
平移示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch03/TranslatedTriangle.js

注意:以后关于矩阵的运算我们使用源码提供的库。旋转使用setRotate,rotate;平移使用setTranslate,translate;缩放使用setScale,scale.
这部分的使用

1
2
3
var formatMatrix = new Matrix4();
formatMatrix.setRotate(ANGLE,0, 0, 1); //绕z轴旋转ANGLE度数
formatMatrix.translate(Tx,Ty,Tz); //x,y,z轴上平移

第一个都要是带z的方法,后面的都不带即可。

https://github.com/zrysmt/data-viz/blob/master/webgl/lib/cuon-matrix.js

动画

动画的原理是使用HTML5 requestAnimationFrame()方法重绘WebGL图形。
示例:https://zrysmt.github.io/demo/webgl-demo/ch04/RotatingTriangle_withButtons.html
示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch04/RotatingTriangle_withButtons.js

4.WebGL二维-颜色与纹理

颜色

示例:https://zrysmt.github.io/demo/webgl-demo/ch05/ColoredTriangle.html
示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch05/ColoredTriangle.js

大概流程是这样的:

顶点坐标==>图形装配==>光栅化==>执行片元着色器

1
2
3
4
5
6
var verticesColors = new Float32Array([
// 坐标x,坐标y,颜色r,g,b
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0,
]);

initVertexBuffers函数对整个矩阵的处理,位置和颜色分别分配给a_Position和a_Color

1
2
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
// Vertex coordinates and color
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0,
]);
var n = 3;

// Create a buffer object
var vertexColorBuffer = gl.createBuffer();
if (!vertexColorBuffer) {
console.log('Failed to create the buffer object');
return false;
}

// Bind the buffer object to target
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

var FSIZE = verticesColors.BYTES_PER_ELEMENT;
//Get the storage location of a_Position, assign and enable buffer
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
gl.enableVertexAttribArray(a_Position); // Enable the assignment of the buffer object

// Get the storage location of a_Position, assign buffer and enable
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
gl.enableVertexAttribArray(a_Color); // Enable the assignment of the buffer object

// Unbind the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, null);

return n;
}

纹理

示例:https://zrysmt.github.io/demo/webgl-demo/ch05/TexturedQuad.html
示例源码:https://github.com/zrysmt/data-viz/blob/master/webgl/ch05/TexturedQuad.js

1
2
3
4
5
6
7
8
9
var image = new Image();  // Create the image object
if (!image) {
console.log('Failed to create the image object');
return false;
}
// Register the event handler to be called on loading an image
image.onload = function(){ loadTexture(gl, n, texture, u_Sampler, image); };
// Tell the browser to load an image
image.src = '../resources/sky.jpg';

逻辑函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function loadTexture(gl, n, texture, u_Sampler, image) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // Flip the image's y axis
// Enable texture unit0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture object to the target
gl.bindTexture(gl.TEXTURE_2D, texture);

// Set the texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Set the texture image
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

// Set the texture unit 0 to the sampler
gl.uniform1i(u_Sampler, 0);

gl.clear(gl.COLOR_BUFFER_BIT); // Clear <canvas>

gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}

三维单独写一篇介绍。

参考阅读:

  • WebGL权威指南
  • webgl开源三维引擎的选择
  • MDN-WebGl API
  • MDN-WebGL 中文教程

使用leaflet或者openlayers 3 调用MapServer服务最佳实践完整说明

发表于 2017-05-04 | 更新于 2018-11-30 | 分类于 WebGIS

最近尝试了很多次,看到网上的很多教程都是版本比较老旧,会出现很多问题。经过数天的实践,现在可以调用成功,遂将步骤记录在此。
为避免以后使用的软件不兼容,首先给出基于window 10的软件版本:

  • MapServer服务器 3.2.1 ms4w-3.2.1-setup.exe

http://www.ms4w.com/

  • QGIS 2.18 osgeo4w-setup-x86_64.exe

http://www.qgis.org/en/site/forusers/download.html


所有配置和示例我都放在了github仓库中mapserver-leaflet-openlayers。

https://github.com/zrysmt/mapserver-leaflet-openlayers

1.安装说明

1.1 ms4w-3.2.1-setup.exe

Windows平台MapServer (MS4W)安装Windows平台MapServer (MS4W)安装

http://blog.csdn.net/u010924834/article/details/53434322
http://mapserver.org/de/introduction.html

MapServer的安装包(ms4w.zip,解压到根目录下)里内置了Apache,不过因为IIS占用了80的端口,所以需要设置下端口 (在\ms4w\Apache\conf\ httpd.conf中,将Listen 80改为Listen 8081)

1.2 osgeo4w-setup-x86_64.exe

因为刚开始值安装了Destop桌面端,后来出了问题,无奈废了很大的劲又卸载了重新安装的。这里建议全部安装,选择Advanced Install 点击选择全部安装即可。

1.3 QGIS几个重要的插件

安装插件
qGIS菜单栏plugin–>Manager and Install Plugin
1.qgis2web
会导出一个完整的网页结构

【Update perview】可以更换坐标系
【Export】导出即可

导出后会打开一个网页,我们需要的是data文件夹下的js,里面有geoJson数据

2.RT Mapfile Export
这是我们发布地图服务的重要的插件

使用插件的时候注意先设置好,打开Manager and Install Plugin对话框,注意在设置中勾选住实验性的插件一项,再搜索插件并进行安装。

2.发布服务

安装好RT Mapfile Export插件后,我们就可以使用QGIS导出Mapfile文件用来发布服务了。
坐标系
首先重要的一点是坐标系一定要选择好。如果不满足,可以使用QGIS工具Reproject Layer转换坐标系。目前常用的是EPSG:3857 墨卡托坐标系和EPSG:4326 WGS84坐标系,这里我们就以3857坐标系为例。

MapFile
在发布服务方面,MapServer有一个和mxd(ArcGIS)类似的map文件来描述地图的图层及样式,不同的是,在使用地图服务的时候,调用的url中需要知道map文件的位置,而ArcGIS则不需要

关于MapFile,这里就不单独介绍太多,Mapfile的作用如下图所示。

基本介绍:
http://blog.csdn.net/qingyafan/article/details/46500127
MapServer 快速入门文档:
http://live.osgeo.org/zh/quickstart/mapserver_quickstart.html

发布过程
因为我把发布的服务器端口改为8000了,所以Online resource url地址是http://127.0.0.1:8000/cgi-bin/mapserv.exe
1.

2.

3.

【确定】即可生成mapfile文件,在本文末尾会给出.
mapfile文件地址在E:\ms4w\apps\worldmap中。
我们在浏览器中输入地址http://127.0.0.1:8000/cgi-bin/mapserv.exe?MAP=E:/ms4w/apps/worldmap/world3857.map&LAYERS=World_region&MODE=MAP
注意:在使用类库调用该WMS服务的时候一定要去掉&MODE=MAP,否则不能成功。

3.使用leaflet或openlayers调用

leaflet示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>leaflet加载MapServer示例子</title>
<!-- <link rel="stylesheet" href="../app/common/css/leaflet.css"> -->
<link rel="stylesheet" href="https://cdn.bootcss.com/leaflet/1.0.3/leaflet.css">
<style type="text/css">
#map {
width: 100%;
height: 600px;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://cdn.bootcss.com/leaflet/1.0.3/leaflet-src.js"></script>
<!-- <script src="../app/common/leaflet-plugin/leaflet-src.js"></script> -->
<script type="text/javascript">
var map = L.map('map', {
crs: L.CRS.EPSG3857
});
map.setView([30, 104], 2);
// let map = L.map('map').setView([30, 104], 10); //默认墨卡托投影 ESPG:3857
//http://127.0.0.1:8000/cgi-bin/mapserv.exe?MAP=E:/ms4w/apps/test/test.map&LAYERS=ALL&MODE=MAP
//一定不要有&MODE=MAP
var myDemo = L.tileLayer.wms("http://127.0.0.1:8000/cgi-bin/mapserv.exe?MAP=E:/ms4w/apps/worldmap/world3857.map", {
layers: 'World_region',
format: 'image/png',
transparent: false,
crs: L.CRS.EPSG3857,
attribution: "自定义地图",
});

myDemo.addTo(map);
</script>
</body>
</html>

访问示例如下:

openlayers示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>openlayers 3加载MapServer示例子</title>
<link rel="stylesheet" href="https://openlayers.org/en/v4.1.1/css/ol.css" type="text/css">
<style type="text/css">
#map {
width: 100%;
height: 600px;
}
</style>
</head>

<body>
<div id="map"></div>
<script src="https://openlayers.org/en/v4.1.1/build/ol.js" type="text/javascript"></script>
<script type="text/javascript">
var layers = [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
new ol.layer.Tile({
source: new ol.source.TileWMS({
url: 'http://127.0.0.1:8000/cgi-bin/mapserv.exe',
params: {
'layers': 'World_region',
'MAP': "E:/ms4w/apps/worldmap/world3857.map",
'TILED': true
},
serverType: 'mapserver',
})
})
];
var map = new ol.Map({
target: 'map',
layers: layers,
view: new ol.View({
center: ol.proj.fromLonLat([104, 30]),
zoom: 2,
projection: 'EPSG:3857'
})
});
</script>
</body>
</html>

访问示例如下:

4.附录:mapfile文件world3857.map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
MAP
FONTSET "E:/ms4w/apps/etc/fonts.txt"
EXTENT -179.99942 -59.484295 180 83.627357
IMAGETYPE "png"
NAME "QGIS--MAP"
SHAPEPATH "E:/ms4w/apps/data/worldmap3857"
SIZE 800 800
STATUS ON
UNITS DD

OUTPUTFORMAT
NAME "png"
MIMETYPE "image/png"
DRIVER "AGG/PNG"
EXTENSION "png"
IMAGEMODE RGB
TRANSPARENT FALSE
END # OUTPUTFORMAT

PROJECTION
"proj=longlat"
"datum=WGS84"
"no_defs"
"init=epsg:3857"
END # PROJECTION
LEGEND
KEYSIZE 20 20
KEYSPACING 5 5
LABEL
SIZE MEDIUM
OFFSET 0 0
SHADOWSIZE 1 1
TYPE BITMAP
END # LABEL
STATUS OFF
END # LEGEND

QUERYMAP
SIZE -1 -1
STATUS OFF
STYLE HILITE
END # QUERYMAP

SCALEBAR
INTERVALS 4
LABEL
SIZE MEDIUM
OFFSET 0 0
SHADOWSIZE 1 1
TYPE BITMAP
END # LABEL
SIZE 200 3
STATUS OFF
UNITS MILES
END # SCALEBAR

WEB
FOOTER ""
HEADER ""
IMAGEPATH "E:/ms4w/tmp/ms_tmp/"
TEMPPATH "E:/ms4w/tmp/"
IMAGEURL "/tmp/"
METADATA
"wms_onlineresource" "http://127.0.0.1:8000/cgi-bin/mapserv.exe?map=E:/ms4w/apps/test2/test2.map"
"wms_enable_request" "*"
"wms_srs" "EPSG:3857"
"wms_feature_info_mime_type" "text/html"
"wms_format" "image/png"
END # METADATA
TEMPLATE "E:/ms4w/apps/test2/test.map.html.tmpl"
END # WEB

LAYER
DATA "E:/ms4w/apps/data/worldmap3857/World_region.shp"
EXTENT -20037443.7774846 -8285806.11600741 20037508.3427892 18422153.0437227
METADATA
"ows_include_items" "all"
"ows_extent" "-20037443.7775 -8285806.11601 20037508.3428 18422153.0437"
"ows_srs" "EPSG:3857"
"ows_title" "World_region"
"wms_getfeatureinfo_formatlist" "OGRGML"
"wms_bbox_extended" "true"
"gml_include_items" "all"
END # METADATA
NAME "World_region"
PROJECTION
"proj=merc"
"a=6378137"
"b=6378137"
"lat_ts=0.0"
"lon_0=0.0"
"x_0=0.0"
"y_0=0"
"k=1.0"
"units=m"
"nadgrids=@null"
"wktext"
"no_defs"
END # PROJECTION
STATUS ON
TILEITEM "location"
TYPE POLYGON
UNITS METERS
CLASS
NAME "Single symbol"
STYLE
COLOR 209 143 80
END # STYLE
STYLE
OUTLINECOLOR 0 0 1
END # STYLE
END # CLASS
END # LAYER

END # MAP

所有配置和示例我都放在了github仓库中mapserver-leaflet-openlayers。

https://github.com/zrysmt/mapserver-leaflet-openlayers

阿里巴巴校招2017前端笔试题目 -- 原生js/html5 实现一个路由

发表于 2017-04-28 | 更新于 2018-11-30 | 分类于 前端技术

阿里巴巴校招2017前端笔试题目:
1)路由有什么缺点?
2)原生js/html5 实现一个路由

缺点:

  • 使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存
  • 单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置

路由的概念:

  • 路由是根据不同的 url 地址展示不同的内容或页面
  • 前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。

我们直接来看两个例子,一个是hash结构的,这是在Html5 的history api出现之前的解决方案;一个是基于history api实现的。

  • hash
1
2
3
http://10.0.0.1/
http://10.0.0.1/#/about
http://10.0.0.1/#/concat
  • history
1
2
3
http://10.0.0.1/
http://10.0.0.1/about
http://10.0.0.1/concat

前端的路由和后端的路由在实现技术上不一样,但是原理都是一样的。

1.hash

关键是监控两个事件,一个是页面加载进来的时候触发load,一个是hash改变的时候触发hashchange。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<ul>
<li><a href="#/">turn white</a></li>
<li><a href="#/blue">turn blue</a></li>
<li><a href="#/green">turn green</a></li>
</ul>
<script>
class Router {
constructor() {
this.routes = {};
this.curUrl = "";
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.curUrl = location.hash.slice(1) || '/';
this.routes[this.curUrl]();
}
init() {
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}

}
var router = new Router();
router.init();
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
content.style.backgroundColor = color;
}
router.route('/', function() {
changeBgColor('white');
});
router.route('/blue', function() {
changeBgColor('blue');
});
router.route('/green', function() {
changeBgColor('green');
});
</script>
</body>

</html>

2.history api

html5 增加了两个方法,分别是pushState,replaceState.

两个方法均有三个参数:一个状态对象、一个标题(现在会被忽略),一个可选的URL地址
状态对象(state object) — 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。

任何可序列化的对象都可以被当做状态对象。因为FireFox浏览器会把状态对象保存到用户的硬盘,这样它们就能在用户重启浏览器之后被还原,我们强行限制状态对象的大小为640k。如果你向pushState()方法传递了一个超过该限额的状态对象,该方法会抛出异常。如果你需要存储很大的数据,建议使用sessionStorage或localStorage。

pushState 用于向 history 添加当前页面的记录,而 replaceState 和 pushState 的用法完全一样,唯一的区别就是它用于修改当前页面在 history 中的记录。

两者的一个表现的区别是:在浏览器上点击后退键的时候,使用pushState的会正常按照点击的顺序依次返回,而使用replaceState的只是替换,不会返回,会直接返回到pushState的记录。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple History</title>
</head>
<body>
<a class="push" href="?push-one">Push One</a>
<a class="push" href="?push-two">Push Two</a>
<a class="push" href="?push-three">Push Three</a>
<a class="replace" href="?replace-one">Replace One</a>
<a class="replace" href="?replace-two">Replace Two</a>
<a class="replace" href="?replace-three">Replace Three</a>
<ul id="log"></ul>
<script src="simple-history.js"></script>
<script src="https://cdn.bootcss.com/jquery/1.7.1/jquery.min.js"></script>
<script>
(function() {
if (!SimpleHistory.supported) {
return;
}
SimpleHistory.start(function(path) {
console.log("match", path);
document.title = "Simple History - " + path;
$("<li>").text("match: " + path).appendTo("#log");
});
$("a:not([href^=http])").click(function(event) {
if (event.metaKey || event.shiftKey || event.ctrlKey) {
return;
}
event.preventDefault();
var path = $(event.target).attr("href");
if ($(event.target).is(".push")) {
SimpleHistory.pushState(event.target.href);
} else {
SimpleHistory.replaceState(event.target.href);
}
});
}())
</script>
</body>

</html>

simple-history.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(function(window, undefined) {

var initial = location.href;

window.SimpleHistory = {
supported: !!(window.history && window.history.pushState),
pushState: function(fragment, state) {
state = state || {};
history.pushState(state, null, fragment);
this.notify(state);
},
replaceState: function(fragment, state) {
state = state || {};
history.replaceState(state, null, fragment);
},
notify: function(state) {
console.log(location.pathname,location.search);
this.matcher(location.pathname + location.search, state);
},
start: function(matcher) {
this.matcher = matcher;
window.addEventListener("popstate", function(event) {
// workaround to always ignore first popstate event (Chrome)
// a timeout isn't reliable enough
if (initial && initial === location.href) {
initial = null;
return;
}
SimpleHistory.notify(event.state || {});
}, false);
}
};

}(window));

参考阅读:

  • 原生JS实现一个简单的前端路由(路由实现的原理)
  • 从 React Router 谈谈路由的那些事

openlayers 3扩展,调用百度地图、高德地图、天地图服务

发表于 2017-03-28 | 更新于 2018-11-30 | 分类于 WebGIS

调用这三个商业地图服务,我们使用的都是切片(Tile)地图服务,关于切片地图的含义这里做简单的介绍:
切片地图就是指将显示的地图切成一块一块的(256 * 256)分别显示加载。openlayers 3中有这样图层加载类,ol.layer.Tile,对应的source类有ol.source.TileImage,ol.source.XYZ,这两者的关系通过源码可以看到
ol.inherits(ol.source.XYZ, ol.source.TileImage);,ol.source.TileImage是父类。

对于天地图,我们访问天地图地图主页服务,打开控制台->Network,我们可以看到请求的一些地址如下:

1
http://t2.tianditu.com/DataServer?T=vec_w&x=53&y=24&l=6'

其中重要的信息是x,y,z分别表示x坐标,y坐标和zoomLevel,

其实在openlayers 3源码中有Bing地图和OSM地图的扩展了,我们可以仿照它们进行一些扩展。

1.扩展天地图

源码使用模块化打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var ol = require('openlayers');

ol.source.TianMap = function(options){
var options = options ? options : {};
var attributions;
if(options.attributions !== undefined){
attributions = option.attributions;
}else{
attributions = [ol.source.BaiduMap.ATTRIBUTION];
}

var url;
if(options.mapType == "sat"){
url = "http://t{0-4}.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}";
}else if(options.mapType == "satLabel"){
url = "http://t{0-4}.tianditu.com/DataServer?T=cia_w&x={x}&y={y}&l={z}";
}else if(options.mapType == "label"){
url = "http://t{0-4}.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}";
}else{
url = "http://t{0-4}.tianditu.com/DataServer?T=vec_w&x={x}&y={y}&l={z}";
}

ol.source.XYZ.call(this, {
attributions: attributions,
projection: ol.proj.get('EPSG:3857'),
cacheSize: options.cacheSize,
crossOrigin: 'anonymous',
opaque: options.opaque !== undefined ? options.opaque : true,
maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileLoadFunction: options.tileLoadFunction,
url: url,
wrapX: options.wrapX
});
}
ol.inherits(ol.source.TianMap, ol.source.XYZ);

ol.source.TianMap.ATTRIBUTION = new ol.Attribution({
html: '&copy; <a class="ol-attribution-tianmap" ' +
'href="http://www.tianditu.cn/">' +
'天地图</a>'
});
module.exports = ol.source.TianMap;

使用方法

1
2
3
4
5
var tianMapSat = new ol.layer.Tile({
title: "天地图卫星",
source: new ol.source.TianMap({mapType:"sat"})
});
map.addLayer(tianMapSat);

2. 扩展百度地图

百度地图坐标进行了加偏,所以需要使用projzh转化
百度地图使用的是定制化的墨卡托投影和BD-09 datum,所以将WGS-84坐标转化为百度坐标需要两步
first transform from WGS-84 to BD-09 (which itself uses the GCJ-09 transform), and then do the forward transform to Baidu Mercator
第一步是将WGS-84 转化为 BD-09,然后转化为百度墨卡托

1
baiduMercator.forward(bd09.fromWGS84(point))

反过来的转化为

1
bd09.toWGS84(baiduMercator.inverse(point))

https://github.com/tschaub/projzh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
var ol = require('openlayers');
var projzh = require('projzh');
/* projzh处理百度坐标的问题,算法基于proj4m project
* https://www.versioneye.com/nodejs/projzh/0.5.0
* https://github.com/tschaub/projzh
*/
ol.source.BaiduMap = function(options){
var options = options ? options : {};

var attributions;
if(options.attributions !== undefined){
attributions = option.attributions;
}else{
attributions = [ol.source.BaiduMap.ATTRIBUTION];
}

var extent = [72.004, 0.8293, 137.8347, 55.8271];

//定义百度坐标
//地址:https://github.com/openlayers/openlayers/issues/3522
var baiduMercator = new ol.proj.Projection({
code: 'baidu',
extent: ol.extent.applyTransform(extent, projzh.ll2bmerc),
units: 'm'
});

ol.proj.addProjection(baiduMercator);
ol.proj.addCoordinateTransforms('EPSG:4326', baiduMercator, projzh.ll2bmerc, projzh.bmerc2ll);
ol.proj.addCoordinateTransforms('EPSG:3857', baiduMercator, projzh.smerc2bmerc, projzh.bmerc2smerc);


var resolutions = [];
for(var i=0; i<19; i++){
resolutions[i] = Math.pow(2, 18-i);
}
var tilegrid = new ol.tilegrid.TileGrid({
origin: [0,0],
resolutions: resolutions,
extent: ol.extent.applyTransform(extent, projzh.ll2bmerc),
tileSize: [256, 256]
});
var satUrls = [0, 1, 2, 3, 4].map(function(sub) {
return 'http://shangetu' + sub +
'.map.bdimg.com/it/u=x={x};y={y};z={z};v=009;type=sate&fm=46&udt=20150601';
});
var urls = [0, 1, 2, 3, 4].map(function(sub) {
return 'http://online' + sub +
'.map.bdimg.com/onlinelabel/qt=tile&x={x}&y={y}&z={z}&v=009&styles=pl&udt=20170301&scaler=1&p=1';
});
ol.source.TileImage.call(this, {
crossOrigin: 'anonymous', //跨域
cacheSize: options.cacheSize,
// projection: ol.proj.get('EPSG:3857'),
projection:'baidu',
tileGrid: tilegrid,
tileUrlFunction: function(tileCoord, pixelRatio, proj){
if(!tileCoord) return "";

var z = tileCoord[0];
var x = tileCoord[1];
var y = tileCoord[2];
var hash = (x << z) + y;
var index = hash % urls.length;
index = index < 0 ? index + urls.length : index;
if(options.mapType == "sat"){
return satUrls[index].replace('{x}', x).replace('{y}', y).replace('{z}', z);
}
return urls[index].replace('{x}', x).replace('{y}', y).replace('{z}', z);

},
wrapX: options.wrapX !== undefined ? options.wrapX : true

});
}

ol.inherits(ol.source.BaiduMap,ol.source.TileImage);

ol.source.BaiduMap.ATTRIBUTION = new ol.Attribution({
html: '&copy; <a class="ol-attribution-baidumap" ' +
'href="http://map.baidu.com/">' +
'百度地图</a>'
});

module.exports = ol.source.BaiduMap;

调用

1
2
3
4
5
var baiduMapSat = new ol.layer.Tile({
title: "百度地图卫星",
source: new ol.source.BaiduMap({mapType:"sat"})
});
map.addLayer(baiduMapSat);

3. 扩展高德地图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var ol = require('openlayers');

ol.source.AMap = function(options){
var options = options ? options : {};

var attributions;
if(options.attributions !== undefined){
attributions = option.attributions;
}else{
attributions = [ol.source.AMap.ATTRIBUTION];
}

var url;
if(options.mapType == "sat"){
url ="http://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}";
}else{
url = "http://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}";
}

ol.source.XYZ.call(this, {
crossOrigin: 'anonymous', //跨域
cacheSize: options.cacheSize,
projection: ol.proj.get('EPSG:3857'),
// urls:urls,
url:url,
wrapX: options.wrapX !== undefined ? options.wrapX : true

});

}

ol.inherits(ol.source.AMap,ol.source.XYZ);


ol.source.AMap.ATTRIBUTION = new ol.Attribution({
html: '&copy; <a class="ol-attribution-amap" ' +
'href="http://ditu.amap.com/">' +
'高德地图</a>'
});

module.exports = ol.source.AMap;

调用

1
2
3
4
5
var aMapSat = new ol.layer.Tile({
title: "高德地图卫星",
source: new ol.source.AMap({mapType:"sat"})
});
map.addLayer(aMapSat);

最后推荐一个github仓库,Openlayers 3 使用React 组件化+wepack+ES6实践,
包括扩展在其中的具体使用方法

https://github.com/zrysmt/openlayers3-react

参考阅读

  • openlayers github
  • openlayers github Issues
  • openlayers 官方地址
  • Openlayers 3 使用React 组件化+wepack+ES6实践记录笔记
  • OpenLayers 3 之 加载百度地图
  • OpenLayers 3 之 加载天地图

openlayers 3 使用React 组件化+wepack+ES6实践记录

发表于 2017-03-10 | 更新于 2018-11-30 | 分类于 WebGIS

本博文不作深入研究内容,只用来记录使用React 组件化+wepack+ES6技术操作Openlayers 3 实践中遇到的问题,本博文作为开篇,所以只是简单的demo案例说明。后面还会有其他的一些博文分享我在项目中遇到的问题和总结的经验。

大约一年前我写过一个系列的Openlayers 3的简单的源码结构的分析,代码以及说明在我的github中有,需要的同学出门右转。

Openlayers 3的简单的源码结构的分析 https://github.com/zrysmt/openlayers-3

我在github上看到一些人将openlayers彻底组件化,属性通过props传入进来,例如:

1
2
3
<layer.Tile>
<source.OSM />
</layer.Tile>

这样做的好处是高度组件化,看起来很和谐。但是这样无形中增加了学习成本和时间成本,我们要看到ol3的API,然后再考虑到转化为组件化的书写的对应形式,导致了多走一步。
本博文的思想很简单,就是外壳用react组件封装,内部的源码实现使用ol3的API完全没有改变,这样就简单清晰而且避免多走一步。具体例子见下面给出的Demo。

我还将我写的一些组件,比如基础地图,工具栏和绘制栏用React写的组件单独从项目中拿出来,提供给使用和学习者一些方便,下面给出地址,欢迎fork,star,不定期更新,有错误请指出:

https://github.com/zrysmt/openlayers3-react

1.一些问题总结

问题1:npm安装的问题:

1
Failed at the closure-util@1.18.0 postinstall script 'node ./bin/closure-util.js update'

解决方案:

1
2
首先 npm i closure-util --save
然后再安装 npm i openlayers --save

问题2:

1
2
3
WARNING in ./~/openlayers/dist/ol.js`
`Critical dependencies:`
`748:1160-1167 This seems to be a pre-built javascript file. Though this is possible, it's not recommended. Try to require the original source to get better results.`

修改webpack.config.js

1
2
module: {// ...
noParse: ['/node_modules/prebuiltlib/dist/build.js',]// ...}

2.基本Demo

olbasemap.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';
import ol from 'openlayers';

import 'openlayers/css/ol.css';
import './olbasemap.scss';

class Olbasemap extends React.Component{
componentDidMount(){
let map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4,
})
});
}
render(){
return(
<div id="map"></div>
)
}
}

export default Olbasemap;

olbasemap.scss

1
2
3
4
#map{
width:100%;
height:600px;
}

3.调用天地图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 基础地图模块
* @Date 2017-3-8
*/
import React from 'react';
import ol from 'openlayers';

import util from '../../../common/util.jsx';

import 'openlayers/css/ol.css';
import './olbasemap.scss';

class Olbasemap extends React.Component{

componentDidMount(){
util.adaptHeight('map',105,300);//高度自适应

let projection,attribution,coor,view;

attribution = new ol.Attribution({
html: '© <a href="http://www.chinaonmap.com/map/index.html">天地图</a>'
});

let map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [attribution],
url: "http://t2.tianditu.com/DataServer?T=vec_w&x={x}&y={y}&l={z}"
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: "http://t2.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}"
})
})
],
view: new ol.View({
// projection: 'EPSG:4326',//WGS84
center: ol.proj.fromLonLat([104, 30]),
zoom: 5,
}),
controls: ol.control.defaults().extend([
new ol.control.FullScreen(), //全屏控件
new ol.control.ScaleLine(), //比例尺
new ol.control.OverviewMap(), //鹰眼控件
new ol.control.Rotate(),
new ol.control.MousePosition(),
new ol.control.ZoomSlider(),
]),
});
}
render(){
return(
<div id="map"></div>
)
}
}

export default Olbasemap;

此时有个问题,假设我们要把map变量传出去,供其他的组件使用,子类和父类之间的传值可以通过props和回调函数完成;现在我们做的组件其实是兄弟关系,怎么将map做成通用呢,经过考虑我们决定使用发布-订阅模式,涉及到各个组件的变量的都在组件内部定义,然后通过发布-订阅模式将一些事件集中管理起来。此外我们还组织将变量放到构造函数中。

于是我们可以这样修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 基础地图模块
* @Date 2017-3-8
*/
import React from 'react';
import ol from 'openlayers';

import util from '../../../common/util.jsx';
import Eventful from '../../../common/Eventful.js';
import olConfig from './ol-config';

import 'openlayers/css/ol.css';
import './olbasemap.scss';

class Olbasemap extends React.Component{
constructor(props){
super(props);
let map,view,projection,attribution,coor,mousePositionControl;

attribution = new ol.Attribution({
html: '© <a href="http://www.chinaonmap.com/map/index.html">天地图</a>'
});
mousePositionControl = new ol.control.MousePosition({
coordinateFormat: ol.coordinate.createStringXY(0),
projection: 'EPSG:3857',//可以是4326 精度应该保留几个小数点
// className: 'custom-mouse-position',
// target: document.getElementById('mouse-position'),
undefinedHTML: '&nbsp;'
});
this.view = view = new ol.View({
// projection: 'EPSG:4326',//WGS84
center: ol.proj.fromLonLat(olConfig.initialView.center||[104, 30]),
zoom: olConfig.initialView.zoom || 5,
});
this.map = map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [attribution],
url: "http://t2.tianditu.com/DataServer?T=vec_w&x={x}&y={y}&l={z}"
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: "http://t2.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}"
})
})
],
view: view,
controls: ol.control.defaults().extend([
new ol.control.FullScreen(), //全屏控件
new ol.control.ScaleLine(), //比例尺
new ol.control.OverviewMap(), //鹰眼控件
new ol.control.Rotate(),
new ol.control.ZoomSlider(),
mousePositionControl
]),
});

Eventful.subscribe('zoomtoall',()=>this.handleClickOfZoomtoall());//订阅
}
handleClickOfZoomtoall(){
this.view.animate({zoom:olConfig.initialView.zoom || 5,
center:ol.proj.fromLonLat(olConfig.initialView.center||[104, 30])});
}
componentDidMount(){
util.adaptHeight('map',105,300);//高度自适应

if(__DEV__) console.info("componentDidMount");

this.map.setTarget(this.refs.map);
}
componentWillUnmount () {
this.map.setTarget(undefined)
}

render(){
return(
<div id="map" ref="map" >
</div>
)
}
}

export default Olbasemap;

在兄弟模块中该调用的模块中调用下面的关键代码即可

1
Eventful.dispatch('zoomtoall');

最后列下来发布-订阅模式的代码Eventful.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 观察者(发布-订阅)模式
* @Date 2017-3-14
*/
let Eventful = {
_events: {},
/**
* [dispatch 发布]
* @param {[String]} evtName [关键字名]
* @param {...[Any]} args [传递的参数]
*/
dispatch(evtName, ...args) {
if (!this._events[evtName]) return;
this._events[evtName].forEach(
func => func.apply(Object.create(null), args));
},
/**
* [subscribe 订阅]
* @param {[String]} evtName [关键字名]
* @param {Function} callback [回掉函数]
*/
subscribe(evtName, callback) {
if (!this._events[evtName]) {
this._events[evtName] = [];
}
this._events[evtName].push(callback);
},
/**
* [unSubscribe 取消订阅]
* @param {[String]} evtName [关键字名]
*/
unSubscribe(evtName) {
if (!this._events[evtName]) return;
delete this._events[evtName];
}
}
export default Eventful;

参考阅读

  • OpenLayers 3 之 加载天地图
12…7

Ruyi Zhao

70 日志
5 分类
52 标签
© 2018 Ruyi Zhao
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.5.0