【Django】GISデータのGooglemap表示とパラメータのグラフ化

#1.はじめに
csvデータをアップロードし、グラフ化・地図表示して表示する、というシンプルなコードが、意外となかったため、本記事を作成。
完成イメージを以下に示す。

![amchart_demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509872/451c0635-355b-4d41-c8dd-083eaf1cdb1a.png)
Fig.1　完成したwebページ

Fig.1の上半分が、amchart.jsの折れ線グラフを使って作成した。x軸、y軸ともに、表示範囲を自由に選択できる。x軸は、時間軸である。
Fig.2の下半分は、GoogleMapAPIを活用して、緯度・経度データを入力して表示できるようにした。


#2.使ったサービス・フレームワークなど
・Google Map API
・Django
・amchart.js

#3.実装
#3-1.構築環境
環境・バージョンは以下の通り。

```
macOS Catalina version 10.15.4
Python 3.7.0
```

#3-2.ディレクトリ構成

```
─root─app─templates─app─base.html
     │   |             └import.html
     │   ├migrations-・・・
     │   ├forms.py
     │   ├views.py
     │   ・・・・                      
     |
     |
     ├project─settings.py
     |       ├urls.py
     |      　・・・
     |
     ├static─js─mychart.js
     |      |  └googlemap.js
     |      └media-temp.csv (<-インポートしたcsvファイルを一時補完するファイル)
     └manage.py
```

Djangoの基本ディレクトリ構成に従っている。


#3-3. コード

```app/views.py
import csv
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import generic
from .forms import CSVUploadForm
from .models import Post


class PostIndex(generic.ListView):
    model = Post


class PostImport(generic.FormView):
    template_name = 'app/import.html'
    success_url = reverse_lazy('app:index')
    form_class = CSVUploadForm

    def form_valid(self, form):
        form.save()
        return redirect('app:index')


def post_export(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="posts.csv"'
    # HttpResponseオブジェクトはファイルっぽいオブジェクトなので、csv.writerにそのまま渡せます。
    writer = csv.writer(response)
    for post in Post.objects.all():
        writer.writerow([post.pk, post.title])
    return response

```

forms.pyが、一番工夫したソースコードになる。

```app/forms.py
import csv
import io
from django import forms
from django.core.validators import FileExtensionValidator
from .models import Post
import codecs

class CSVUploadForm(forms.Form):
    file = forms.FileField(
        label='CSVファイル',
        help_text='※拡張子csvのファイルをアップロードしてください。',
        validators=[FileExtensionValidator(allowed_extensions=['csv'])]
    )

    def clean_file(self):
        file = self.cleaned_data['file']

        print("file:",file)

        # csv.readerに渡すため、TextIOWrapperでテキストモードなファイルに変換
        csv_file = io.TextIOWrapper(file, encoding='utf-8')
        print("csv_file",csv_file)
        reader = csv.reader(csv_file)
        print("reader",reader)
        
        self.csv_file = reader
        
        for row in reader:
            #print("reader row", row)
            #print("line_num", reader.line_num)
            if reader.line_num == 1:
                with open("static/media/temp.csv", mode="w", encoding="utf-8") as f:
                    print("row in x before", row)
                    row = ",".join(row) 
                    row = row + "\n" 
                    print("row in x after", row)
                    f.write(row)
                print("option=x")

            else:
                with open("static/media/temp.csv", mode="a", encoding="utf-8") as f:
                    #print("row in a before", row)
                    #print(type(row))
                    row = ",".join(row) 
                    row = row + "\n" 
                    #print("row in a after", row)
                    #print(type(row))
                    
                    f.write(row)
                #print("option=a")


        # 各行から作った保存前のモデルインスタンスを保管するリスト

        self._instances = []
        
        try:
            for row in reader:
                post = Post(pk=row[0], title=row[1])
                self._instances.append(post)

        except UnicodeDecodeError:
            raise forms.ValidationError('ファイルのエンコーディングや、正しいCSVファイルか確認ください。')

        return file

    def save(self):
        Post.objects.bulk_create(self._instances, ignore_conflicts=True)
        Post.objects.bulk_update(self._instances, fields=['title'])
        print("save content in save func", Post)
```


```app/urls.py
from django.urls import path
from . import views

app_name = 'app'

urlpatterns = [
    path('', views.PostIndex.as_view(), name='index'),
    path('import/', views.PostImport.as_view(), name='import'),
    path('export/', views.post_export, name='export'),
]
```

以下のソースのうち、[API-KEY]はAPI-Keyを挿入する。Google_API-Keyは、各自で発行ください。


```app/templates/app/base.html
<!doctype html>
{% load static %}
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <style>
      #chartdiv {
        width: 100%;
        height: 500px;
      }
      #map {
        width: 100%;
        height: 400px;
      }
      </style>
      
      <!-- Resources -->
      <script src="https://www.amcharts.com/lib/4/core.js"></script>
      <script src="https://www.amcharts.com/lib/4/charts.js"></script>
      <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
      
      <!-- Chart code -->
      <script src="{% static 'js/mychart.js' %}"></script>
    <title>ローバーセンサ　可視化</title>
  </head>

  <body>
    <ul class="nav justify-content-center">
      <li class="nav-item">
        <a class="nav-link" href="{% url 'app:index' %}">Index</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="{% url 'app:import' %}">CSV読み込み</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="{% url 'app:export' %}">CSV出力</a>
      </li>
    </ul>
  <div class="container">
    {% block content %}{% endblock %}
      
  
    <div id="map"></div>
      
    <script  src="{% static 'js/googlemap.js' %}"></script>
    <script src="https://maps.googleapis.com/maps/api/js?key=[API-KEY]"
async defer></script>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
  </div>

  </body>
</html>

```


```app/templates/app/import.html
{% extends 'app/base.html' %}

{% block content %}
<form action="" method="POST" enctype="multipart/form-data">
  {{ form.as_ul }}
  {% csrf_token %}
  <button type="submit">送信</button>
</form>
{% endblock %}

```


```app/templates/app/post_list.html
{% extends 'app/base.html' %}

{% block content %}
<div id="chartdiv"></div>
{% endblock %}
```

```project/static/js/mychart.js
// 2) CSVから２次元配列に変換
function csv2Array(str) {
    var csvData = [];
    var lines = str.split("\n");
    for (var i = 0; i < lines.length; ++i) {
      var cells = lines[i].split(",");
      csvData.push(cells);
    }
    return csvData;
  }
  
  function drawBarChart(data) {
    // 3)chart.jsのdataset用の配列を用意
    var tmpLabels = [], tmpData1 = [], tmpData2 = [];
    for (var row in data) {
      tmpLabels.push(data[row][0])
      tmpData1.push(data[row][1])
      tmpData2.push(data[row][2])
    };
  
    // 4)chart.jsで描画
    var ctx = document.getElementById("myChart").getContext("2d");
    var myChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: tmpLabels,
        datasets: [
          { label: "Tokyo", data: tmpData1, backgroundColor: "red" },
          { label: "Osaka", data: tmpData2, backgroundColor: "blue" }
        ]
      }
    });
  }
  
  function makeLineChart(data) {
    am4core.ready(function() {
            
      // Themes begin
      am4core.useTheme(am4themes_animated);
      // Themes end
      
      // Create chart instance
      var chart = am4core.create("chartdiv", am4charts.XYChart);
      
      // Add data
      console.log(data);
      console.log(typeof data);
      console.log(data[1][1]);
      chart.data = generateChartData(data); //　追加
      console.log(chart.data);
      
      // Create axes
      var xAxis = chart.xAxes.push(new am4charts.ValueAxis());
  
      //xAxis.renderer.minGridDistance = 150;
      xAxis.title.text = "Time (sec)";
      
      var yAxis = chart.yAxes.push(new am4charts.ValueAxis());
      yAxis.title.text = "ax";
      
      // Create series
      var series = chart.series.push(new am4charts.LineSeries());
      series.dataFields.valueX = "timestamp";
      series.dataFields.valueY ="ax";
      series.name = "ax";
    
      series.strokeWidth = 2;
      series.minBulletDistance = 10;
      series.tooltipText = "{valueY}";
      series.tooltip.pointerOrientation = "vertical";
      series.tooltip.background.cornerRadius = 20;
      series.tooltip.background.fillOpacity = 0.5;
      series.tooltip.label.padding(12,12,12,12)
  
      var series2 = chart.series.push(new am4charts.LineSeries());
      series2.dataFields.valueX = "timestamp";
      series2.dataFields.valueY = "ay";
      series2.name = "ay";
  
  
      var series3 = chart.series.push(new am4charts.LineSeries());
      series3.dataFields.valueX = "timestamp";
      series3.dataFields.valueY = "az";
      series3.name = "az";
  
      // Create y axis range
      var range = yAxis.axisRanges.create();
      range.label.disabled = false;
      range.label.rotation = 270;
  
      // Create x axis range
      var range_x = xAxis.axisRanges.create();
      range_x.label.disabled = false;
      range_x.label.rotation = 0;
  
  
      // Add scrollbar
      chart.scrollbarX = new am4charts.XYChartScrollbar();
      chart.scrollbarX.series.push(series);
      chart.scrollbarX.series.push(series2);
      chart.scrollbarX.series.push(series3);
      chart.scrollbarY = new am4charts.XYChartScrollbar();
      chart.scrollbarY.series.push(series);
      chart.scrollbarY.series.push(series2);
      chart.scrollbarY.series.push(series3);
      
      // Add cursor
      chart.cursor = new am4charts.XYCursor();
      chart.cursor.xAxis = xAxis;
      chart.cursor.yAxis = yAxis;
      chart.cursor.snapToSeries = series;
      chart.cursor.snapToSeries = series2;
      chart.cursor.snapToSeries = series3;
      
      // Add legend
      chart.legend = new am4charts.Legend();
  
      function generateChartData(data) {
          var chartData = [];
          var index_length = Number(data.length);
          const initial_time = data[0][0]
          console.log(index_length);
          for (var i = 0; i < (index_length-1); i++) {
      
              var data_line = data[i];
  
              chartData.push({
                timestamp: (data_line[0]-initial_time)/1000,
                ax: data_line[1],
                ay: data_line[2],
                az: data_line[3],
                wx: data_line[4],
                wy: data_line[5],
                wz: data_line[6],
                mx: data_line[7],
                my: data_line[8],
                mz: data_line[9],
                lat: data_line[10],
                lng: data_line[11],
                yaw: data_line[12]              
            });
          }
  
          return chartData;
      }
      
      });
  }
  
  function main(file_name) {
    // 1) ajaxでCSVファイルをロード
    var req = new XMLHttpRequest();
    var filePath = file_name//'acc_gyro.csv';
    req.open("GET", filePath, true);
    req.onload = function() {
      // 2) CSVデータ変換の呼び出し
      data = csv2Array(req.responseText);
      // 3) amChart.jsデータ準備、4) amChart.js描画の呼び出し
      makeLineChart(data);
    }
    req.send(null);
  }

  main("/static/media/temp.csv");
  ```

```static/js/googlemap.js
// 2) CSVから２次元配列に変換
function csv2Array(str) {
    var csvData = [];
    var lines = str.split("\n");
    for (var i = 0; i < lines.length; ++i) {
      var cells = lines[i].split(",");
      csvData.push(cells);
    }
    return csvData;
  }

// ２次元配列にプロパティと
function mapData(data) {
    var mapData = [];
    const initial_time = data[0][0]
    var index_length = Number(data.length);
    for (var i = 0; i < (index_length-1); i++) {

        var data_line = data[i];

        mapData.push({
          timestamp: (data_line[0]-initial_time)/1000,
          ax: data_line[1],
          ay: data_line[2],
          az: data_line[3],
          wx: data_line[4],
          wy: data_line[5],
          wz: data_line[6],
          mx: data_line[7],
          my: data_line[8],
          mz: data_line[9],
          lat: data_line[10],
          lng: data_line[11],
          yaw: data_line[12]              
      });
    }
    return mapData;
}


function initMap(data) {
  var map;
  mapdata = mapData(data);
  var index_length = Number(data.length);
  console.log(mapdata);
  console.log(index_length);
  initial_loc = mapdata[0];

  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: Number(initial_loc.lat), lng: Number(initial_loc.lng)},
    zoom: 15
  });
  
　for(var i = 1; i < (index_length-1); i++) {
    location_line = mapdata[i];
    //console.log(i); 
    //console.log(location_line);  
    // Markerの初期設定
    var markerOpts = {
    position: {lat: Number(location_line.lat), lng: Number(location_line.lng)},
    map: map,
    title: "mark"
    };
    // 直前で作成したMarkerOptionsを利用してMarkerを作成
    var marker = new google.maps.Marker(markerOpts);
  }
  
}

function main(file_name) {
    // 1) ajaxでCSVファイルをロード
    var req = new XMLHttpRequest();
    var filePath = file_name;//'static/js/acc_gyro.csv';
    req.open("GET", filePath, true);
    req.onload = function() {
      // 2) CSVデータ変換の呼び出し
      data = csv2Array(req.responseText);
      // 3) google mapデータ準備、4) google map描画の呼び出し
      initMap(data);
    }
    req.send(null);
  }

main("/static/media/temp.csv");
```

```project/setting.py
(前略)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

```

#4.所感
・Djangoを使って、初めて作ったwebアプリになる。感慨深い。
・staticディレクトリの設定方法は、tutorialに記載がなかったので、非常に勉強になった。
・次は、AWSのEC2インスタンスへのデプロイ結果を報告したい。

#5.参考文献
amchartの実装方法：https://www.suzu6.net/posts/56-amcharts-samples/
csvアップロード機能の実装：https://qiita.com/t-iguchi/items/d2862e7ef7ec7f1b07e5