# 十七.Raster

# 1.阴影浮雕

查看代码详情
<template>
  <div>
    <div ref="map" class="map"></div>
    <table class="controls">
      <tr>
        <td><label for="vert">vertical exaggeration:</label></td>
        <td><input ref="vert" type="range" min="1" max="5" value="1" /></td>
        <td><span ref="vertOut"></span> x</td>
      </tr>
      <tr>
        <td><label for="sunEl">sun elevation:</label></td>
        <td><input ref="sunEl" type="range" min="0" max="90" value="45" /></td>
        <td><span ref="sunElOut"></span> °</td>
      </tr>
      <tr>
        <td><label for="sunAz">sun azimuth:</label></td>
        <td><input ref="sunAz" type="range" min="0" max="360" value="45" /></td>
        <td><span ref="sunAzOut"></span> °</td>
      </tr>
    </table>
  </div>
</template>
  
  <script>
export default {
  mounted() {
    let {
      Map,
      View,
      layer: { Tile: TileLayer, Image: ImageLayer },
      source: { OSM, Raster, XYZ },
    } = ol;
    function shade(inputs, data) {
      const elevationImage = inputs[0];
      const width = elevationImage.width;
      const height = elevationImage.height;
      const elevationData = elevationImage.data;
      const shadeData = new Uint8ClampedArray(elevationData.length);
      const dp = data.resolution * 2;
      const maxX = width - 1;
      const maxY = height - 1;
      const pixel = [0, 0, 0, 0];
      const twoPi = 2 * Math.PI;
      const halfPi = Math.PI / 2;
      const sunEl = (Math.PI * data.sunEl) / 180;
      const sunAz = (Math.PI * data.sunAz) / 180;
      const cosSunEl = Math.cos(sunEl);
      const sinSunEl = Math.sin(sunEl);
      let pixelX,
        pixelY,
        x0,
        x1,
        y0,
        y1,
        offset,
        z0,
        z1,
        dzdx,
        dzdy,
        slope,
        aspect,
        cosIncidence,
        scaled;
      function calculateElevation(pixel) {
        return pixel[0] + pixel[1] * 2 + pixel[2] * 3;
      }
      for (pixelY = 0; pixelY <= maxY; ++pixelY) {
        y0 = pixelY === 0 ? 0 : pixelY - 1;
        y1 = pixelY === maxY ? maxY : pixelY + 1;
        for (pixelX = 0; pixelX <= maxX; ++pixelX) {
          x0 = pixelX === 0 ? 0 : pixelX - 1;
          x1 = pixelX === maxX ? maxX : pixelX + 1;
          offset = (pixelY * width + x0) * 4;
          pixel[0] = elevationData[offset];
          pixel[1] = elevationData[offset + 1];
          pixel[2] = elevationData[offset + 2];
          pixel[3] = elevationData[offset + 3];
          z0 = data.vert * calculateElevation(pixel);
          offset = (pixelY * width + x1) * 4;
          pixel[0] = elevationData[offset];
          pixel[1] = elevationData[offset + 1];
          pixel[2] = elevationData[offset + 2];
          pixel[3] = elevationData[offset + 3];
          z1 = data.vert * calculateElevation(pixel);
          dzdx = (z1 - z0) / dp;
          offset = (y0 * width + pixelX) * 4;
          pixel[0] = elevationData[offset];
          pixel[1] = elevationData[offset + 1];
          pixel[2] = elevationData[offset + 2];
          pixel[3] = elevationData[offset + 3];
          z0 = data.vert * calculateElevation(pixel);
          offset = (y1 * width + pixelX) * 4;
          pixel[0] = elevationData[offset];
          pixel[1] = elevationData[offset + 1];
          pixel[2] = elevationData[offset + 2];
          pixel[3] = elevationData[offset + 3];
          z1 = data.vert * calculateElevation(pixel);
          dzdy = (z1 - z0) / dp;
          slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy));
          aspect = Math.atan2(dzdy, -dzdx);
          if (aspect < 0) {
            aspect = halfPi - aspect;
          } else if (aspect > halfPi) {
            aspect = twoPi - aspect + halfPi;
          } else {
            aspect = halfPi - aspect;
          }
          cosIncidence =
            sinSunEl * Math.cos(slope) +
            cosSunEl * Math.sin(slope) * Math.cos(sunAz - aspect);
          offset = (pixelY * width + pixelX) * 4;
          scaled = 255 * cosIncidence;
          shadeData[offset] = scaled;
          shadeData[offset + 1] = scaled;
          shadeData[offset + 2] = scaled;
          shadeData[offset + 3] = elevationData[offset + 3];
        }
      }
      return { data: shadeData, width: width, height: height };
    }
    const elevation = new XYZ({
      url: "https://{a-d}.tiles.mapbox.com/v3/aj.sf-dem/{z}/{x}/{y}.png",
      crossOrigin: "anonymous",
    });
    const raster = new Raster({
      sources: [elevation],
      operationType: "image",
      operation: shade,
    });
    const map = new Map({
      target: this.$refs.map,
      layers: [
        new TileLayer({
          source: new OSM(),
        }),
        new ImageLayer({
          opacity: 0.3,
          source: raster,
        }),
      ],
      view: new View({
        extent: [-13675026, 4439648, -13580856, 4580292],
        center: [-13615645, 4497969],
        minZoom: 10,
        maxZoom: 16,
        zoom: 13,
      }),
    });
    const controlIds = ["vert", "sunEl", "sunAz"];
    const controls = {};
    controlIds.forEach(
      function (id) {
        const control = this.$refs[id];
        const output = this.$refs[id + "Out"];
        const listener = function () {
          output.innerText = control.value;
          raster.changed();
        };
        control.addEventListener("input", listener);
        control.addEventListener("change", listener);
        output.innerText = control.value;
        controls[id] = control;
      }.bind(this)
    );
    raster.on("beforeoperations", function (event) {
      const data = event.data;
      data.resolution = event.resolution;
      for (const id in controls) {
        data[id] = Number(controls[id].value);
      }
    });
  },
};
</script>
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173

# 2.光栅源

查看代码详情
<template>
  <div class="rel">
    <div ref="map" class="map"></div>
    <div id="plot"></div>
  </div>
</template>

<script>
export default {
  mounted() {
    let {
      Map,
      View,
      layer: { Tile: TileLayer, Image: ImageLayer },
      source: { XYZ, Raster: RasterSource },
    } = ol

    const minVgi = 0
    const maxVgi = 0.5
    const bins = 10
    function vgi(pixel) {
      const r = pixel[0] / 255
      const g = pixel[1] / 255
      const b = pixel[2] / 255
      return (2 * g - r - b) / (2 * g + r + b)
    }
    function summarize(value, counts) {
      const min = counts.min
      const max = counts.max
      const num = counts.values.length
      if (value < min) {
      } else if (value >= max) {
        counts.values[num - 1] += 1
      } else {
        const index = Math.floor((value - min) / counts.delta)
        counts.values[index] += 1
      }
    }
    const attributions =
      '<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
      '<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>'
    const aerial = new XYZ({
      attributions: attributions,
      url:
        "https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=" +
        mapkeys.maptiler,
      maxZoom: 20,
      crossOrigin: "",
    })
    const raster = new RasterSource({
      sources: [aerial],
      operation: function (pixels, data) {
        const pixel = pixels[0]
        const value = vgi(pixel)
        summarize(value, data.counts)
        if (value >= data.threshold) {
          pixel[0] = 0
          pixel[1] = 255
          pixel[2] = 0
          pixel[3] = 128
        } else {
          pixel[3] = 0
        }
        return pixel
      },
      lib: {
        vgi: vgi,
        summarize: summarize,
      },
    })
    raster.set("threshold", 0.25)
    function createCounts(min, max, num) {
      const values = new Array(num)
      for (let i = 0; i < num; ++i) {
        values[i] = 0
      }
      return {
        min: min,
        max: max,
        values: values,
        delta: (max - min) / num,
      }
    }
    raster.on("beforeoperations", function (event) {
      event.data.counts = createCounts(minVgi, maxVgi, bins)
      event.data.threshold = raster.get("threshold")
    })
    raster.on("afteroperations", function (event) {
      schedulePlot(event.resolution, event.data.counts, event.data.threshold)
    })
    const map = new Map({
      layers: [
        new TileLayer({
          source: aerial,
        }),
        new ImageLayer({
          source: raster,
        }),
      ],
      target: this.$refs.map,
      view: new View({
        center: [-9651695, 4937351],
        zoom: 13,
        minZoom: 12,
        maxZoom: 19,
      }),
    })
    let timer = null
    function schedulePlot(resolution, counts, threshold) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      timer = setTimeout(
        plot.bind(null, resolution, counts, threshold),
        1000 / 60
      )
    }
    const barWidth = 15
    const plotHeight = 150
    const chart = d3
      .select("#plot")
      .append("svg")
      .attr("width", barWidth * bins)
      .attr("height", plotHeight)
    const chartRect = chart.node().getBoundingClientRect()
    const tip = d3.select(document.body).append("div").attr("class", "tip")
    function plot(resolution, counts, threshold) {
      const yScale = d3
        .scaleLinear()
        .domain([0, d3.max(counts.values)])
        .range([0, plotHeight])
      const bar = chart.selectAll("rect").data(counts.values)
      bar.enter().append("rect")
      bar
        .attr("class", function (count, index) {
          const value = counts.min + index * counts.delta
          return "bar" + (value >= threshold ? " selected" : "")
        })
        .attr("width", barWidth - 2)
      bar
        .transition()
        .attr("transform", function (value, index) {
          return (
            "translate(" +
            index * barWidth +
            ", " +
            (plotHeight - yScale(value)) +
            ")"
          )
        })
        .attr("height", yScale)
      bar.on("mousemove", function () {
        const index = bar.nodes().indexOf(this)
        const threshold = counts.min + index * counts.delta
        if (raster.get("threshold") !== threshold) {
          raster.set("threshold", threshold)
          raster.changed()
        }
      })
      bar.on("mouseover", function (event) {
        const index = bar.nodes().indexOf(this)
        let area = 0
        for (let i = counts.values.length - 1; i >= index; --i) {
          area += resolution * resolution * counts.values[i]
        }
        tip.html(message(counts.min + index * counts.delta, area))
        tip.style("display", "block")
        tip
          .transition()
          .style(
            "left",
            chartRect.left + index * barWidth + barWidth / 2 + "px"
          )
          .style("top", event.y - 60 + "px")
          .style("opacity", 1)
      })
      bar.on("mouseout", function () {
        tip
          .transition()
          .style("opacity", 0)
          .on("end", function () {
            tip.style("display", "none")
          })
      })
    }
    function message(value, area) {
      const acres = (area / 4046.86)
        .toFixed(0)
        .replace(/\B(?=(\d{3})+(?!\d))/g, ",")
      return acres + " acres at<br>" + value.toFixed(2) + " VGI or above"
    }
  },
}
</script>
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194

# 3.颜色处理

查看代码详情
<template>
  <div>
    <div ref="map" class="map"></div>
    <table class="controls">
      <tr>
        <td><label for="hue">颜色</label></td>
        <td><input id="hue" type="range" min="-180" max="180" value="0" /></td>
        <td><span id="hueOut"></span> °&nbsp;</td>
      </tr>
      <tr>
        <td><label for="chroma">色度</label></td>
        <td>
          <input id="chroma" type="range" min="0" max="100" value="100" />
        </td>
        <td><span id="chromaOut"></span> %</td>
      </tr>
      <tr>
        <td><label for="lightness">亮度</label></td>
        <td>
          <input id="lightness" type="range" min="0" max="100" value="100" />
        </td>
        <td><span id="lightnessOut"></span> %</td>
      </tr>
    </table>
  </div>
</template>

<script>
export default {
  mounted() {
    let {
      Map,
      View,
      layer: { Image: ImageLayer },
      source: { Raster: RasterSource, Stamen },
    } = ol
    const Xn = 0.95047
    const Yn = 1
    const Zn = 1.08883
    const t0 = 4 / 29
    const t1 = 6 / 29
    const t2 = 3 * t1 * t1
    const t3 = t1 * t1 * t1
    const twoPi = 2 * Math.PI

    function rgb2hcl(pixel) {
      const red = rgb2xyz(pixel[0])
      const green = rgb2xyz(pixel[1])
      const blue = rgb2xyz(pixel[2])
      const x = xyz2lab(
        (0.4124564 * red + 0.3575761 * green + 0.1804375 * blue) / Xn
      )
      const y = xyz2lab(
        (0.2126729 * red + 0.7151522 * green + 0.072175 * blue) / Yn
      )
      const z = xyz2lab(
        (0.0193339 * red + 0.119192 * green + 0.9503041 * blue) / Zn
      )
      const l = 116 * y - 16
      const a = 500 * (x - y)
      const b = 200 * (y - z)
      const c = Math.sqrt(a * a + b * b)
      let h = Math.atan2(b, a)
      if (h < 0) {
        h += twoPi
      }
      pixel[0] = h
      pixel[1] = c
      pixel[2] = l
      return pixel
    }

    function hcl2rgb(pixel) {
      const h = pixel[0]
      const c = pixel[1]
      const l = pixel[2]
      const a = Math.cos(h) * c
      const b = Math.sin(h) * c
      let y = (l + 16) / 116
      let x = isNaN(a) ? y : y + a / 500
      let z = isNaN(b) ? y : y - b / 200
      y = Yn * lab2xyz(y)
      x = Xn * lab2xyz(x)
      z = Zn * lab2xyz(z)
      pixel[0] = xyz2rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z)
      pixel[1] = xyz2rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z)
      pixel[2] = xyz2rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z)
      return pixel
    }

    function xyz2lab(t) {
      return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0
    }

    function lab2xyz(t) {
      return t > t1 ? t * t * t : t2 * (t - t0)
    }

    function rgb2xyz(x) {
      return (x /= 255) <= 0.04045
        ? x / 12.92
        : Math.pow((x + 0.055) / 1.055, 2.4)
    }

    function xyz2rgb(x) {
      return (
        255 *
        (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055)
      )
    }

    const raster = new RasterSource({
      sources: [
        new Stamen({
          layer: "watercolor",
        }),
      ],
      operation: function (pixels, data) {
        const hcl = rgb2hcl(pixels[0])

        let h = hcl[0] + (Math.PI * data.hue) / 180
        if (h < 0) {
          h += twoPi
        } else if (h > twoPi) {
          h -= twoPi
        }
        hcl[0] = h

        hcl[1] *= data.chroma / 100
        hcl[2] *= data.lightness / 100

        return hcl2rgb(hcl)
      },
      lib: {
        rgb2hcl: rgb2hcl,
        hcl2rgb: hcl2rgb,
        rgb2xyz: rgb2xyz,
        lab2xyz: lab2xyz,
        xyz2lab: xyz2lab,
        xyz2rgb: xyz2rgb,
        Xn: Xn,
        Yn: Yn,
        Zn: Zn,
        t0: t0,
        t1: t1,
        t2: t2,
        t3: t3,
        twoPi: twoPi,
      },
    })

    const controls = {}

    raster.on("beforeoperations", function (event) {
      const data = event.data
      for (const id in controls) {
        data[id] = Number(controls[id].value)
      }
    })

    const map = new Map({
      layers: [
        new ImageLayer({
          source: raster,
        }),
      ],
      target: this.$refs.map,
      view: new View({
        center: [0, 2500000],
        zoom: 2,
        maxZoom: 18,
      }),
    })

    const controlIds = ["hue", "chroma", "lightness"]
    controlIds.forEach(function (id) {
      const control = document.getElementById(id)
      const output = document.getElementById(id + "Out")
      const listener = function () {
        output.innerText = control.value
        raster.changed()
      }
      control.addEventListener("input", listener)
      control.addEventListener("change", listener)
      output.innerText = control.value
      controls[id] = control
    })
  },
}
</script>
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189