学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Django3.2 | クラウドソーシングアプリの構築 | 55 | 配達人のマップ表示

↓↓クリックして頂けると励みになります。


54 | Web Socketの設定】 << 【ホーム】 >> 【56 | マップ リアルタイム表示


Redisをインストールします。
5分くらいかかります。
コマンド
brew install redis


channels-redisをインストールします。
コマンド
pip install channels-redis==3.2.0


バージョンを確認します。
コマンド
pip freeze > requirements.txt
確認 【Desktop/crowdsource/requirements.txt】

aioredis==1.3.1
asgiref==3.7.2
async-timeout==4.0.3
attrs==23.1.0
autobahn==23.6.2
Automat==22.10.0
beautifulsoup4==4.12.2
CacheControl==0.13.1
cachetools==5.3.1
certifi==2023.7.22
cffi==1.15.1
channels==3.0.3
channels-redis==3.2.0
chardet==3.0.4
charset-normalizer==3.2.0
constantly==15.1.0
cryptography==41.0.3
daphne==3.0.2
defusedxml==0.7.1
Django==3.2.20
django-bootstrap4==2.3.1
firebase-admin==4.4.0
google-api-core==1.34.0
google-api-python-client==2.97.0
google-auth==2.22.0
google-auth-httplib2==0.1.0
google-cloud-core==2.3.3
google-cloud-firestore==2.11.1
google-cloud-storage==2.10.0
google-crc32c==1.5.0
google-resumable-media==2.5.0
googleapis-common-protos==1.60.0
grpcio==1.57.0
grpcio-status==1.48.2
hiredis==2.2.3
httplib2==0.22.0
hyperlink==21.0.0
idna==2.10
incremental==22.10.0
msgpack==1.0.5
oauthlib==3.2.2
paypalrestsdk==1.13.1
Pillow==10.0.0
proto-plus==1.22.3
protobuf==3.20.3
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
PyJWT==2.8.0
pyOpenSSL==23.2.0
pyparsing==3.1.1
python3-openid==3.2.0
pytz==2023.3
requests==2.25.0
requests-oauthlib==1.3.1
rsa==4.9
service-identity==23.1.0
six==1.16.0
social-auth-app-django==4.0.0
social-auth-core==4.4.2
soupsieve==2.5
sqlparse==0.4.4
stripe==2.55.1
Twisted==23.8.0
txaio==23.1.1
typing_extensions==4.7.1
uritemplate==4.1.1
urllib3==1.26.16
zope.interface==6.0



ターミナルを分割し、Redisサーバーを起動します。
コマンド
redis-server


「crowdsource/settings.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/crowdsource/settings.py】179行目(末尾)

# Channels
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}



「core/consumers.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/consumers.py】

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from . import models

class JobConsumer(WebsocketConsumer):
  def connect(self):
    self.job_id = self.scope['url_route']['kwargs']['job_id']
    self.job_group_name = 'job_%s' % self.job_id

    # Join room group
    async_to_sync(self.channel_layer.group_add)(
      self.job_group_name,
      self.channel_name
    )

    self.accept()

  def disconnect(self, close_code):
    # Leave room group
    async_to_sync(self.channel_layer.group_discard)(
      self.job_group_name,
      self.channel_name
    )

  def receive(self, text_data):
    text_data_json = json.loads(text_data)
    job = text_data_json['job']

    # print("Job", job)

    if job.get('courier_lat') and job.get('courier_lng'):
      self.scope['user'].courier.lat = job['courier_lat']
      self.scope['user'].courier.lng = job['courier_lng']
      self.scope['user'].courier.save()

    # Send message to job group
    async_to_sync(self.channel_layer.group_send)(
      self.job_group_name,
      {
        'type': 'job_update',
        'job': job
      }
    )

  # Receive message from job group
  def job_update(self, event):
    job = event['job']

    # Send message to WebSocket
    self.send(text_data=json.dumps({
      'job': job
    }))



「core/templates/customer/job.html」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/templates/customer/job.html】

{% extends 'customer/base.html' %}
{% load static %}

{% block head %}

<style>
  .photo {
    object-fit: cover;
  }
  .photo-blank {
    border: 2px dashed #DFDFDF;
    height: 130px;
    width: 130px;
    border-radius: 5px;
    align-items: center;
    display: flex;
    justify-content: center;
    text-align: center;
    padding: 10px;
  }
</style>

<script
  src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly"
  defer></script>

<script>
  var pickupLat = parseFloat(" {{ job.pickup_lat }} ");
  var pickupLng = parseFloat(" {{ job.pickup_lng }} ");
  var deliveryLat = parseFloat(" {{ job.delivery_lat }} ");
  var deliveryLng = parseFloat(" {{ job.delivery_lng }} ");

  var courierLat = parseFloat(" {{ job.courier.lat }} ");
  var courierLng = parseFloat(" {{ job.courier.lng }} ");

  function initMap() {
    const directionsService = new google.maps.DirectionsService();
    const directionsRenderer = new google.maps.DirectionsRenderer();
    const map = new google.maps.Map(document.getElementById("map"), {
        zoom: 7,
        center: { lat: 43.062087, lng: 141.354404 },
    });
    directionsRenderer.setMap(map);
    calculateAndDisplayRoute(map, directionsService, directionsRenderer);
  }

  function calculateAndDisplayRoute(map, directionsService, directionsRenderer) {
    directionsService.route(
      {
        origin: new google.maps.LatLng(pickupLat, pickupLng),
        destination: new google.maps.LatLng(deliveryLat, deliveryLng),
        travelMode: google.maps.TravelMode.DRIVING,
      },
      (response, status) => {
        if (status === "OK") {
          new google.maps.DirectionsRenderer({
            map: map,
            directions: response,
            suppressMarkers: true,
            polylineOptions: {
              strokeColor: "#000",
              strokeWeight: 5,
              strokeOpacity: 0.8
            }
          });

          var leg = response.routes[0].legs[0];
          new google.maps.Marker({
            position: leg.start_location,
            map: map,
            icon: "{% static 'img/start.png' %}"
          });

          new google.maps.Marker({
            position: leg.end_location,
            map: map,
            icon: "{% static 'img/end.png' %}"
          });

          window.courierMarker = new google.maps.Marker({
            position: new google.maps.LatLng(courierLat, courierLng),
            map,
            icon: '/static/img/courier.png',
          })
        } else {
          window.alert("リクエストは次の理由で失敗しました:" + status);
        }
      }
    );
  }

</script>

{% endblock %}

{% block main %}
<!-- JOB DESCRIPTION -->
<div class="media mb-4">
  <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150">
  <div class="media-body">
    {% if job.status == 'processing' %}
    <form method="POST" class="float-right">
      {% csrf_token %}
      <button type="submit" class="btn btn-warning">配達依頼をキャンセル</button>
    </form>
    {% endif %}
    <h4>{{ job.name }}</h4>
    <p class="text-secondary">{{ job.description }}</p>
    <div class="row">
      <div class="col-lg-3">
        <small class="text-secondary">カテゴリー</small><br />
        <span><b>{{ job.category.name }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">サイズ</small><br />
        <span><b>{{ job.get_size_display }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">料金</small><br />
        <span><b>{{ job.price }}円</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">数量</small><br />
        <span><b>{{ job.quantity }}</b></span>
      </div>
    </div>
  </div>
</div>
<!-- DELIVERY INFORMATION -->
<b class="text-secondary">配達依頼情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body p-4">
    <h4 class="mb-3">
      荷物受取先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>荷物受取先住所</b><br />
        <span>{{ job.pickup_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.pickup_name }}</b><br />
        <span>{{ job.pickup_phone }}</span>
      </div>
      <div id="pickup_photo" class="col-lg-4">
        {% if job.pickup_photo %}
        <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>
    </div>
    <hr class="my-4" />
    <h4 class="mb-3">
      配達先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>配達先住所</b><br />
        <span>{{ job.delivery_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.delivery_name }}</b><br />
        <span>{{ job.delivery_phone }}</span>
      </div>
      <div id="delivery_photo" class="col-lg-4">
        {% if job.delivery_photo %}
        <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>
    </div>
  </div>
</div>
<!-- マップ -->
<div class="d-flex justify-content-between">
    <b class="text-secondary">配達の追跡</b>
    <div>
        <span id="job_status" class="badge badge-warning">
            {% if job.get_status_display == 'Processing' %}
            進行中 {{ job.get_status_display }}
        {% else %}
            完了 {{ job.get_status_display }}
        {% endif %}
    
        </span>
    </div>
  </div>
  
  <div class="card bg-white mt-2">
    <div class="card-body p-0">
      <div id="map" style="height: 500px;"></div>
    </div>
  </div>
{% endblock %}



ブラウザを確認します。
配達依頼を受けた配達人の位置が配達依頼をした人のマップに表示されるようになりました。

配達人の位置表示
配達人の位置表示



引き続き「core/templates/customer/job.html」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/templates/customer/job.html】

{% extends 'customer/base.html' %}
{% load static %}

{% block head %}

<style>
  .photo {
    object-fit: cover;
  }
  .photo-blank {
    border: 2px dashed #DFDFDF;
    height: 130px;
    width: 130px;
    border-radius: 5px;
    align-items: center;
    display: flex;
    justify-content: center;
    text-align: center;
    padding: 10px;
  }
</style>

<script
  src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly"
  defer></script>

<script>
  var pickupLat = parseFloat(" {{ job.pickup_lat }} ");
  var pickupLng = parseFloat(" {{ job.pickup_lng }} ");
  var deliveryLat = parseFloat(" {{ job.delivery_lat }} ");
  var deliveryLng = parseFloat(" {{ job.delivery_lng }} ");

  var courierLat = parseFloat(" {{ job.courier.lat }} ");
  var courierLng = parseFloat(" {{ job.courier.lng }} ");

  function initMap() {
    const directionsService = new google.maps.DirectionsService();
    const directionsRenderer = new google.maps.DirectionsRenderer();
    const map = new google.maps.Map(document.getElementById("map"), {
        zoom: 7,
        center: { lat: 43.062087, lng: 141.354404 },
    });
    directionsRenderer.setMap(map);
    calculateAndDisplayRoute(map, directionsService, directionsRenderer);
  }

  function calculateAndDisplayRoute(map, directionsService, directionsRenderer) {
    directionsService.route(
      {
        origin: new google.maps.LatLng(pickupLat, pickupLng),
        destination: new google.maps.LatLng(deliveryLat, deliveryLng),
        travelMode: google.maps.TravelMode.DRIVING,
      },
      (response, status) => {
        if (status === "OK") {
          new google.maps.DirectionsRenderer({
            map: map,
            directions: response,
            suppressMarkers: true,
            polylineOptions: {
              strokeColor: "#000",
              strokeWeight: 5,
              strokeOpacity: 0.8
            }
          });

          var leg = response.routes[0].legs[0];
          new google.maps.Marker({
            position: leg.start_location,
            map: map,
            icon: "{% static 'img/start.png' %}"
          });

          new google.maps.Marker({
            position: leg.end_location,
            map: map,
            icon: "{% static 'img/end.png' %}"
          });

          window.courierMarker = new google.maps.Marker({
            position: new google.maps.LatLng(courierLat, courierLng),
            map,
            icon: '/static/img/courier.png',
          })
        } else {
          window.alert("リクエストは次の理由で失敗しました:" + status);
        }
      }
    );
  }

  const jobSocket = new WebSocket(
      "ws://" + window.location.host + "/ws/jobs/{{ job.id }}/"
    );

  // このページが WebSocket 経由でイベントを受信するたびにこの関数を実行します
  jobSocket.onmessage = function (e) {
    var data = JSON.parse(e.data);
    var job = data.job;
    console.log(job);

    if (job.courier_lat && job.courier_lng) {
      var courierPosition = new google.maps.LatLng(job.courier_lat, job.courier_lng);
      window.courierMarker.setPosition(courierPosition);
    }

  }  

</script>

{% endblock %}

{% block main %}
<!-- JOB DESCRIPTION -->
<div class="media mb-4">
  <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150">
  <div class="media-body">
    {% if job.status == 'processing' %}
    <form method="POST" class="float-right">
      {% csrf_token %}
      <button type="submit" class="btn btn-warning">配達依頼をキャンセル</button>
    </form>
    {% endif %}
    <h4>{{ job.name }}</h4>
    <p class="text-secondary">{{ job.description }}</p>
    <div class="row">
      <div class="col-lg-3">
        <small class="text-secondary">カテゴリー</small><br />
        <span><b>{{ job.category.name }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">サイズ</small><br />
        <span><b>{{ job.get_size_display }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">料金</small><br />
        <span><b>{{ job.price }}円</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">数量</small><br />
        <span><b>{{ job.quantity }}</b></span>
      </div>
    </div>
  </div>
</div>
<!-- DELIVERY INFORMATION -->
<b class="text-secondary">配達依頼情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body p-4">
    <h4 class="mb-3">
      荷物受取先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>荷物受取先住所</b><br />
        <span>{{ job.pickup_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.pickup_name }}</b><br />
        <span>{{ job.pickup_phone }}</span>
      </div>
      <div id="pickup_photo" class="col-lg-4">
        {% if job.pickup_photo %}
        <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>
    </div>
    <hr class="my-4" />
    <h4 class="mb-3">
      配達先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>配達先住所</b><br />
        <span>{{ job.delivery_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.delivery_name }}</b><br />
        <span>{{ job.delivery_phone }}</span>
      </div>
      <div id="delivery_photo" class="col-lg-4">
        {% if job.delivery_photo %}
        <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>
    </div>
  </div>
</div>
<!-- マップ -->
<div class="d-flex justify-content-between">
    <b class="text-secondary">配達の追跡</b>
    <div>
        <span id="job_status" class="badge badge-warning">
            {% if job.get_status_display == 'Processing' %}
            進行中 {{ job.get_status_display }}
        {% else %}
            完了 {{ job.get_status_display }}
        {% endif %}
    
        </span>
    </div>
  </div>
  
  <div class="card bg-white mt-2">
    <div class="card-body p-0">
      <div id="map" style="height: 500px;"></div>
    </div>
  </div>
{% endblock %}



配達人の位置が配達依頼人の依頼ページで連動して見られるようになりました。

配達人の位置連動
配達人の位置連動


↓↓クリックして頂けると励みになります。


54 | Web Socketの設定】 << 【ホーム】 >> 【56 | マップ リアルタイム表示