You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

791 lines
23 KiB

9 years ago
  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <title>Highcharts Example</title>
  6. <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
  7. <style type="text/css">
  8. ${demo.css}
  9. </style>
  10. <script type="text/javascript">
  11. /**
  12. * This is a complex demo of how to set up a Highcharts chart, coupled to a
  13. * dynamic source and extended by drawing image sprites, wind arrow paths
  14. * and a second grid on top of the chart. The purpose of the demo is to inpire
  15. * developers to go beyond the basic chart types and show how the library can
  16. * be extended programmatically. This is what the demo does:
  17. *
  18. * - Loads weather forecast from www.yr.no in form of an XML service. The XML
  19. * is translated on the Higcharts website into JSONP for the sake of the demo
  20. * being shown on both our website and JSFiddle.
  21. * - When the data arrives async, a Meteogram instance is created. We have
  22. * created the Meteogram prototype to provide an organized structure of the different
  23. * methods and subroutines associated with the demo.
  24. * - The parseYrData method parses the data from www.yr.no into several parallel arrays. These
  25. * arrays are used directly as the data option for temperature, precipitation
  26. * and air pressure. As the temperature data gives only full degrees, we apply
  27. * some smoothing on the graph, but keep the original data in the tooltip.
  28. * - After this, the options structure is build, and the chart generated with the
  29. * parsed data.
  30. * - In the callback (on chart load), we weather icons on top of the temperature series.
  31. * The icons are sprites from a single PNG image, placed inside a clipped 30x30
  32. * SVG <g> element. VML interprets this as HTML images inside a clipped div.
  33. * - Lastly, the wind arrows are built and added below the plot area, and a grid is
  34. * drawn around them. The wind arrows are basically drawn north-south, then rotated
  35. * as per the wind direction.
  36. */
  37. function Meteogram(xml, container) {
  38. // Parallel arrays for the chart data, these are populated as the XML/JSON file
  39. // is loaded
  40. this.symbols = [];
  41. this.symbolNames = [];
  42. this.precipitations = [];
  43. this.windDirections = [];
  44. this.windDirectionNames = [];
  45. this.windSpeeds = [];
  46. this.windSpeedNames = [];
  47. this.temperatures = [];
  48. this.pressures = [];
  49. // Initialize
  50. this.xml = xml;
  51. this.container = container;
  52. // Run
  53. this.parseYrData();
  54. }
  55. /**
  56. * Return weather symbol sprites as laid out at http://om.yr.no/forklaring/symbol/
  57. */
  58. Meteogram.prototype.getSymbolSprites = function (symbolSize) {
  59. return {
  60. '01d': {
  61. x: 0,
  62. y: 0
  63. },
  64. '01n': {
  65. x: symbolSize,
  66. y: 0
  67. },
  68. '16': {
  69. x: 2 * symbolSize,
  70. y: 0
  71. },
  72. '02d': {
  73. x: 0,
  74. y: symbolSize
  75. },
  76. '02n': {
  77. x: symbolSize,
  78. y: symbolSize
  79. },
  80. '03d': {
  81. x: 0,
  82. y: 2 * symbolSize
  83. },
  84. '03n': {
  85. x: symbolSize,
  86. y: 2 * symbolSize
  87. },
  88. '17': {
  89. x: 2 * symbolSize,
  90. y: 2 * symbolSize
  91. },
  92. '04': {
  93. x: 0,
  94. y: 3 * symbolSize
  95. },
  96. '05d': {
  97. x: 0,
  98. y: 4 * symbolSize
  99. },
  100. '05n': {
  101. x: symbolSize,
  102. y: 4 * symbolSize
  103. },
  104. '18': {
  105. x: 2 * symbolSize,
  106. y: 4 * symbolSize
  107. },
  108. '06d': {
  109. x: 0,
  110. y: 5 * symbolSize
  111. },
  112. '06n': {
  113. x: symbolSize,
  114. y: 5 * symbolSize
  115. },
  116. '07d': {
  117. x: 0,
  118. y: 6 * symbolSize
  119. },
  120. '07n': {
  121. x: symbolSize,
  122. y: 6 * symbolSize
  123. },
  124. '08d': {
  125. x: 0,
  126. y: 7 * symbolSize
  127. },
  128. '08n': {
  129. x: symbolSize,
  130. y: 7 * symbolSize
  131. },
  132. '19': {
  133. x: 2 * symbolSize,
  134. y: 7 * symbolSize
  135. },
  136. '09': {
  137. x: 0,
  138. y: 8 * symbolSize
  139. },
  140. '10': {
  141. x: 0,
  142. y: 9 * symbolSize
  143. },
  144. '11': {
  145. x: 0,
  146. y: 10 * symbolSize
  147. },
  148. '12': {
  149. x: 0,
  150. y: 11 * symbolSize
  151. },
  152. '13': {
  153. x: 0,
  154. y: 12 * symbolSize
  155. },
  156. '14': {
  157. x: 0,
  158. y: 13 * symbolSize
  159. },
  160. '15': {
  161. x: 0,
  162. y: 14 * symbolSize
  163. },
  164. '20d': {
  165. x: 0,
  166. y: 15 * symbolSize
  167. },
  168. '20n': {
  169. x: symbolSize,
  170. y: 15 * symbolSize
  171. },
  172. '20m': {
  173. x: 2 * symbolSize,
  174. y: 15 * symbolSize
  175. },
  176. '21d': {
  177. x: 0,
  178. y: 16 * symbolSize
  179. },
  180. '21n': {
  181. x: symbolSize,
  182. y: 16 * symbolSize
  183. },
  184. '21m': {
  185. x: 2 * symbolSize,
  186. y: 16 * symbolSize
  187. },
  188. '22': {
  189. x: 0,
  190. y: 17 * symbolSize
  191. },
  192. '23': {
  193. x: 0,
  194. y: 18 * symbolSize
  195. }
  196. };
  197. };
  198. /**
  199. * Function to smooth the temperature line. The original data provides only whole degrees,
  200. * which makes the line graph look jagged. So we apply a running mean on it, but preserve
  201. * the unaltered value in the tooltip.
  202. */
  203. Meteogram.prototype.smoothLine = function (data) {
  204. var i = data.length,
  205. sum,
  206. value;
  207. while (i--) {
  208. data[i].value = value = data[i].y; // preserve value for tooltip
  209. // Set the smoothed value to the average of the closest points, but don't allow
  210. // it to differ more than 0.5 degrees from the given value
  211. sum = (data[i - 1] || data[i]).y + value + (data[i + 1] || data[i]).y;
  212. data[i].y = Math.max(value - 0.5, Math.min(sum / 3, value + 0.5));
  213. }
  214. };
  215. /**
  216. * Callback function that is called from Highcharts on hovering each point and returns
  217. * HTML for the tooltip.
  218. */
  219. Meteogram.prototype.tooltipFormatter = function (tooltip) {
  220. // Create the header with reference to the time interval
  221. var index = tooltip.points[0].point.index,
  222. ret = '<small>' + Highcharts.dateFormat('%A, %b %e, %H:%M', tooltip.x) + '-' +
  223. Highcharts.dateFormat('%H:%M', tooltip.points[0].point.to) + '</small><br>';
  224. // Symbol text
  225. ret += '<b>' + this.symbolNames[index] + '</b>';
  226. ret += '<table>';
  227. // Add all series
  228. Highcharts.each(tooltip.points, function (point) {
  229. var series = point.series;
  230. ret += '<tr><td><span style="color:' + series.color + '">\u25CF</span> ' + series.name +
  231. ': </td><td style="white-space:nowrap">' + Highcharts.pick(point.point.value, point.y) +
  232. series.options.tooltip.valueSuffix + '</td></tr>';
  233. });
  234. // Add wind
  235. ret += '<tr><td style="vertical-align: top">\u25CF Wind</td><td style="white-space:nowrap">' + this.windDirectionNames[index] +
  236. '<br>' + this.windSpeedNames[index] + ' (' +
  237. Highcharts.numberFormat(this.windSpeeds[index], 1) + ' m/s)</td></tr>';
  238. // Close
  239. ret += '</table>';
  240. return ret;
  241. };
  242. /**
  243. * Draw the weather symbols on top of the temperature series. The symbols are sprites of a single
  244. * file, defined in the getSymbolSprites function above.
  245. */
  246. Meteogram.prototype.drawWeatherSymbols = function (chart) {
  247. var meteogram = this,
  248. symbolSprites = this.getSymbolSprites(30);
  249. $.each(chart.series[0].data, function (i, point) {
  250. var sprite,
  251. group;
  252. if (meteogram.resolution > 36e5 || i % 2 === 0) {
  253. sprite = symbolSprites[meteogram.symbols[i]];
  254. if (sprite) {
  255. // Create a group element that is positioned and clipped at 30 pixels width and height
  256. group = chart.renderer.g()
  257. .attr({
  258. translateX: point.plotX + chart.plotLeft - 15,
  259. translateY: point.plotY + chart.plotTop - 30,
  260. zIndex: 5
  261. })
  262. .clip(chart.renderer.clipRect(0, 0, 30, 30))
  263. .add();
  264. // Position the image inside it at the sprite position
  265. chart.renderer.image(
  266. 'http://www.highcharts.com/samples/graphics/meteogram-symbols-30px.png',
  267. -sprite.x,
  268. -sprite.y,
  269. 90,
  270. 570
  271. )
  272. .add(group);
  273. }
  274. }
  275. });
  276. };
  277. /**
  278. * Create wind speed symbols for the Beaufort wind scale. The symbols are rotated
  279. * around the zero centerpoint.
  280. */
  281. Meteogram.prototype.windArrow = function (name) {
  282. var level,
  283. path;
  284. // The stem and the arrow head
  285. path = [
  286. 'M', 0, 7, // base of arrow
  287. 'L', -1.5, 7,
  288. 0, 10,
  289. 1.5, 7,
  290. 0, 7,
  291. 0, -10 // top
  292. ];
  293. level = $.inArray(name, ['Calm', 'Light air', 'Light breeze', 'Gentle breeze', 'Moderate breeze',
  294. 'Fresh breeze', 'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm',
  295. 'Violent storm', 'Hurricane']);
  296. if (level === 0) {
  297. path = [];
  298. }
  299. if (level === 2) {
  300. path.push('M', 0, -8, 'L', 4, -8); // short line
  301. } else if (level >= 3) {
  302. path.push(0, -10, 7, -10); // long line
  303. }
  304. if (level === 4) {
  305. path.push('M', 0, -7, 'L', 4, -7);
  306. } else if (level >= 5) {
  307. path.push('M', 0, -7, 'L', 7, -7);
  308. }
  309. if (level === 5) {
  310. path.push('M', 0, -4, 'L', 4, -4);
  311. } else if (level >= 6) {
  312. path.push('M', 0, -4, 'L', 7, -4);
  313. }
  314. if (level === 7) {
  315. path.push('M', 0, -1, 'L', 4, -1);
  316. } else if (level >= 8) {
  317. path.push('M', 0, -1, 'L', 7, -1);
  318. }
  319. return path;
  320. };
  321. /**
  322. * Draw the wind arrows. Each arrow path is generated by the windArrow function above.
  323. */
  324. Meteogram.prototype.drawWindArrows = function (chart) {
  325. var meteogram = this;
  326. $.each(chart.series[0].data, function (i, point) {
  327. var sprite, arrow, x, y;
  328. if (meteogram.resolution > 36e5 || i % 2 === 0) {
  329. // Draw the wind arrows
  330. x = point.plotX + chart.plotLeft + 7;
  331. y = 255;
  332. if (meteogram.windSpeedNames[i] === 'Calm') {
  333. arrow = chart.renderer.circle(x, y, 10).attr({
  334. fill: 'none'
  335. });
  336. } else {
  337. arrow = chart.renderer.path(
  338. meteogram.windArrow(meteogram.windSpeedNames[i])
  339. ).attr({
  340. rotation: parseInt(meteogram.windDirections[i], 10),
  341. translateX: x, // rotation center
  342. translateY: y // rotation center
  343. });
  344. }
  345. arrow.attr({
  346. stroke: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black',
  347. 'stroke-width': 1.5,
  348. zIndex: 5
  349. })
  350. .add();
  351. }
  352. });
  353. };
  354. /**
  355. * Draw blocks around wind arrows, below the plot area
  356. */
  357. Meteogram.prototype.drawBlocksForWindArrows = function (chart) {
  358. var xAxis = chart.xAxis[0],
  359. x,
  360. pos,
  361. max,
  362. isLong,
  363. isLast,
  364. i;
  365. for (pos = xAxis.min, max = xAxis.max, i = 0; pos <= max + 36e5; pos += 36e5, i += 1) {
  366. // Get the X position
  367. isLast = pos === max + 36e5;
  368. x = Math.round(xAxis.toPixels(pos)) + (isLast ? 0.5 : -0.5);
  369. // Draw the vertical dividers and ticks
  370. if (this.resolution > 36e5) {
  371. isLong = pos % this.resolution === 0;
  372. } else {
  373. isLong = i % 2 === 0;
  374. }
  375. chart.renderer.path(['M', x, chart.plotTop + chart.plotHeight + (isLong ? 0 : 28),
  376. 'L', x, chart.plotTop + chart.plotHeight + 32, 'Z'])
  377. .attr({
  378. 'stroke': chart.options.chart.plotBorderColor,
  379. 'stroke-width': 1
  380. })
  381. .add();
  382. }
  383. };
  384. /**
  385. * Get the title based on the XML data
  386. */
  387. Meteogram.prototype.getTitle = function () {
  388. return 'Meteogram for ' + this.xml.location.name + ', ' + this.xml.location.country;
  389. };
  390. /**
  391. * Build and return the Highcharts options structure
  392. */
  393. Meteogram.prototype.getChartOptions = function () {
  394. var meteogram = this;
  395. return {
  396. chart: {
  397. renderTo: this.container,
  398. marginBottom: 70,
  399. marginRight: 40,
  400. marginTop: 50,
  401. plotBorderWidth: 1,
  402. width: 800,
  403. height: 310
  404. },
  405. title: {
  406. text: this.getTitle(),
  407. align: 'left'
  408. },
  409. credits: {
  410. text: 'Forecast from <a href="http://yr.no">yr.no</a>',
  411. href: this.xml.credit.link['@attributes'].url,
  412. position: {
  413. x: -40
  414. }
  415. },
  416. tooltip: {
  417. shared: true,
  418. useHTML: true,
  419. formatter: function () {
  420. return meteogram.tooltipFormatter(this);
  421. }
  422. },
  423. xAxis: [{ // Bottom X axis
  424. type: 'datetime',
  425. tickInterval: 2 * 36e5, // two hours
  426. minorTickInterval: 36e5, // one hour
  427. tickLength: 0,
  428. gridLineWidth: 1,
  429. gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0',
  430. startOnTick: false,
  431. endOnTick: false,
  432. minPadding: 0,
  433. maxPadding: 0,
  434. offset: 30,
  435. showLastLabel: true,
  436. labels: {
  437. format: '{value:%H}'
  438. }
  439. }, { // Top X axis
  440. linkedTo: 0,
  441. type: 'datetime',
  442. tickInterval: 24 * 3600 * 1000,
  443. labels: {
  444. format: '{value:<span style="font-size: 12px; font-weight: bold">%a</span> %b %e}',
  445. align: 'left',
  446. x: 3,
  447. y: -5
  448. },
  449. opposite: true,
  450. tickLength: 20,
  451. gridLineWidth: 1
  452. }],
  453. yAxis: [{ // temperature axis
  454. title: {
  455. text: null
  456. },
  457. labels: {
  458. format: '{value}°',
  459. style: {
  460. fontSize: '10px'
  461. },
  462. x: -3
  463. },
  464. plotLines: [{ // zero plane
  465. value: 0,
  466. color: '#BBBBBB',
  467. width: 1,
  468. zIndex: 2
  469. }],
  470. // Custom positioner to provide even temperature ticks from top down
  471. tickPositioner: function () {
  472. var max = Math.ceil(this.max) + 1,
  473. pos = max - 12, // start
  474. ret;
  475. if (pos < this.min) {
  476. ret = [];
  477. while (pos <= max) {
  478. ret.push(pos += 1);
  479. }
  480. } // else return undefined and go auto
  481. return ret;
  482. },
  483. maxPadding: 0.3,
  484. tickInterval: 1,
  485. gridLineColor: (Highcharts.theme && Highcharts.theme.background2) || '#F0F0F0'
  486. }, { // precipitation axis
  487. title: {
  488. text: null
  489. },
  490. labels: {
  491. enabled: false
  492. },
  493. gridLineWidth: 0,
  494. tickLength: 0
  495. }, { // Air pressure
  496. allowDecimals: false,
  497. title: { // Title on top of axis
  498. text: 'hPa',
  499. offset: 0,
  500. align: 'high',
  501. rotation: 0,
  502. style: {
  503. fontSize: '10px',
  504. color: Highcharts.getOptions().colors[2]
  505. },
  506. textAlign: 'left',
  507. x: 3
  508. },
  509. labels: {
  510. style: {
  511. fontSize: '8px',
  512. color: Highcharts.getOptions().colors[2]
  513. },
  514. y: 2,
  515. x: 3
  516. },
  517. gridLineWidth: 0,
  518. opposite: true,
  519. showLastLabel: false
  520. }],
  521. legend: {
  522. enabled: false
  523. },
  524. plotOptions: {
  525. series: {
  526. pointPlacement: 'between'
  527. }
  528. },
  529. series: [{
  530. name: 'Temperature',
  531. data: this.temperatures,
  532. type: 'spline',
  533. marker: {
  534. enabled: false,
  535. states: {
  536. hover: {
  537. enabled: true
  538. }
  539. }
  540. },
  541. tooltip: {
  542. valueSuffix: '°C'
  543. },
  544. zIndex: 1,
  545. color: '#FF3333',
  546. negativeColor: '#48AFE8'
  547. }, {
  548. name: 'Precipitation',
  549. data: this.precipitations,
  550. type: 'column',
  551. color: '#68CFE8',
  552. yAxis: 1,
  553. groupPadding: 0,
  554. pointPadding: 0,
  555. borderWidth: 0,
  556. shadow: false,
  557. dataLabels: {
  558. enabled: true,
  559. formatter: function () {
  560. if (this.y > 0) {
  561. return this.y;
  562. }
  563. },
  564. style: {
  565. fontSize: '8px'
  566. }
  567. },
  568. tooltip: {
  569. valueSuffix: 'mm'
  570. }
  571. }, {
  572. name: 'Air pressure',
  573. color: Highcharts.getOptions().colors[2],
  574. data: this.pressures,
  575. marker: {
  576. enabled: false
  577. },
  578. shadow: false,
  579. tooltip: {
  580. valueSuffix: ' hPa'
  581. },
  582. dashStyle: 'shortdot',
  583. yAxis: 2
  584. }]
  585. }
  586. };
  587. /**
  588. * Post-process the chart from the callback function, the second argument to Highcharts.Chart.
  589. */
  590. Meteogram.prototype.onChartLoad = function (chart) {
  591. this.drawWeatherSymbols(chart);
  592. this.drawWindArrows(chart);
  593. this.drawBlocksForWindArrows(chart);
  594. };
  595. /**
  596. * Create the chart. This function is called async when the data file is loaded and parsed.
  597. */
  598. Meteogram.prototype.createChart = function () {
  599. var meteogram = this;
  600. this.chart = new Highcharts.Chart(this.getChartOptions(), function (chart) {
  601. meteogram.onChartLoad(chart);
  602. });
  603. };
  604. /**
  605. * Handle the data. This part of the code is not Highcharts specific, but deals with yr.no's
  606. * specific data format
  607. */
  608. Meteogram.prototype.parseYrData = function () {
  609. var meteogram = this,
  610. xml = this.xml,
  611. pointStart;
  612. if (!xml || !xml.forecast) {
  613. $('#loading').html('<i class="fa fa-frown-o"></i> Failed loading data, please try again later');
  614. return;
  615. }
  616. // The returned xml variable is a JavaScript representation of the provided XML,
  617. // generated on the server by running PHP simple_load_xml and converting it to
  618. // JavaScript by json_encode.
  619. $.each(xml.forecast.tabular.time, function (i, time) {
  620. // Get the times - only Safari can't parse ISO8601 so we need to do some replacements
  621. var from = time['@attributes'].from + ' UTC',
  622. to = time['@attributes'].to + ' UTC';
  623. from = from.replace(/-/g, '/').replace('T', ' ');
  624. from = Date.parse(from);
  625. to = to.replace(/-/g, '/').replace('T', ' ');
  626. to = Date.parse(to);
  627. if (to > pointStart + 4 * 24 * 36e5) {
  628. return;
  629. }
  630. // If it is more than an hour between points, show all symbols
  631. if (i === 0) {
  632. meteogram.resolution = to - from;
  633. }
  634. // Populate the parallel arrays
  635. meteogram.symbols.push(time.symbol['@attributes']['var'].match(/[0-9]{2}[dnm]?/)[0]);
  636. meteogram.symbolNames.push(time.symbol['@attributes'].name);
  637. meteogram.temperatures.push({
  638. x: from,
  639. y: parseInt(time.temperature['@attributes'].value),
  640. // custom options used in the tooltip formatter
  641. to: to,
  642. index: i
  643. });
  644. meteogram.precipitations.push({
  645. x: from,
  646. y: parseFloat(time.precipitation['@attributes'].value)
  647. });
  648. meteogram.windDirections.push(parseFloat(time.windDirection['@attributes'].deg));
  649. meteogram.windDirectionNames.push(time.windDirection['@attributes'].name);
  650. meteogram.windSpeeds.push(parseFloat(time.windSpeed['@attributes'].mps));
  651. meteogram.windSpeedNames.push(time.windSpeed['@attributes'].name);
  652. meteogram.pressures.push({
  653. x: from,
  654. y: parseFloat(time.pressure['@attributes'].value)
  655. });
  656. if (i == 0) {
  657. pointStart = (from + to) / 2;
  658. }
  659. });
  660. // Smooth the line
  661. this.smoothLine(this.temperatures);
  662. // Create the chart when the data is loaded
  663. this.createChart();
  664. };
  665. // End of the Meteogram protype
  666. $(function () { // On DOM ready...
  667. // Set the hash to the yr.no URL we want to parse
  668. if (!location.hash) {
  669. var place = 'United_Kingdom/England/London';
  670. //place = 'France/Rhône-Alpes/Val_d\'Isère~2971074';
  671. //place = 'Norway/Sogn_og_Fjordane/Vik/Målset';
  672. //place = 'United_States/California/San_Francisco';
  673. //place = 'United_States/Minnesota/Minneapolis';
  674. location.hash = 'http://www.yr.no/place/' + place + '/forecast_hour_by_hour.xml';
  675. }
  676. // Then get the XML file through Highcharts' jsonp provider, see
  677. // https://github.com/highslide-software/highcharts.com/blob/master/samples/data/jsonp.php
  678. // for source code.
  679. $.getJSON(
  680. 'http://www.highcharts.com/samples/data/jsonp.php?url=' + location.hash.substr(1) + '&callback=?',
  681. function (xml) {
  682. var meteogram = new Meteogram(xml, 'container');
  683. }
  684. );
  685. });
  686. </script>
  687. </head>
  688. <body>
  689. <script src="../../js/highcharts.js"></script>
  690. <script src="../../js/modules/exporting.js"></script>
  691. <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
  692. <div id="container" style="width: 800px; height: 310px; margin: 0 auto">
  693. <div style="margin-top: 100px; text-align: center" id="loading">
  694. <i class="fa fa-spinner fa-spin"></i> Loading data from external source
  695. </div>
  696. </div>
  697. <!--
  698. <div style="width: 800px; margin: 0 auto">
  699. <a href="#http://www.yr.no/place/United_Kingdom/England/London/forecast_hour_by_hour.xml">London</a>,
  700. <a href="#http://www.yr.no/place/France/Rhône-Alpes/Val_d\'Isère~2971074/forecast_hour_by_hour.xml">Val d'Isère</a>,
  701. <a href="#http://www.yr.no/place/United_States/California/San_Francisco/forecast_hour_by_hour.xml">San Francisco</a>,
  702. <a href="#http://www.yr.no/place/Norway/Vik/Vikafjell/forecast_hour_by_hour.xml">Vikjafjellet</a>
  703. </div>
  704. -->
  705. </body>
  706. </html>