Kartenansicht

This commit is contained in:
Daniel Grams 2020-10-11 11:16:09 +02:00
parent 5c13c3dfe5
commit a3601802ec
6 changed files with 507 additions and 23 deletions

View File

@ -217,4 +217,17 @@ $( function() {
alert("Browser doesn't support geolocation!");
}
});
});
});
String.prototype.truncate = String.prototype.truncate ||
function ( n, useWordBoundary ){
if (this.length <= n) { return this; }
const subString = this.substr(0, n-1); // the original check
return (useWordBoundary
? subString.substr(0, subString.lastIndexOf(" "))
: subString) + "&hellip;";
};
function scroll_to_element(element, complete) {
$('html, body').animate({ scrollTop: element.offset().top }, { duration: 'slow', complete: complete });
}

199
static/svg-icon.js Normal file
View File

@ -0,0 +1,199 @@
//Leaflet-SVGIcon
//SVG icon for any marker class
//Ilya Atkin
//ilya.atkin@unh.edu
L.DivIcon.SVGIcon = L.DivIcon.extend({
options: {
"circleText": "",
"className": "svg-icon",
"circleAnchor": null, //defaults to [iconSize.x/2, iconSize.x/2]
"circleColor": null, //defaults to color
"circleOpacity": null, // defaults to opacity
"circleFillColor": "rgb(255,255,255)",
"circleFillOpacity": null, //default to opacity
"circleRatio": 0.5,
"circleWeight": null, //defaults to weight
"color": "rgb(0,102,255)",
"fillColor": null, // defaults to color
"fillOpacity": 0.4,
"fontColor": "rgb(0, 0, 0)",
"fontOpacity": "1",
"fontSize": null, // defaults to iconSize.x/4
"fontWeight": "normal",
"iconAnchor": null, //defaults to [iconSize.x/2, iconSize.y] (point tip)
"iconSize": L.point(32,48),
"opacity": 1,
"popupAnchor": null,
"weight": 2
},
initialize: function(options) {
options = L.Util.setOptions(this, options)
//iconSize needs to be converted to a Point object if it is not passed as one
options.iconSize = L.point(options.iconSize)
//in addition to setting option dependant defaults, Point-based options are converted to Point objects
if (!options.circleAnchor) {
options.circleAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.x)/2)
}
else {
options.circleAnchor = L.point(options.circleAnchor)
}
if (!options.circleColor) {
options.circleColor = options.color
}
if (!options.circleFillOpacity) {
options.circleFillOpacity = options.opacity
}
if (!options.circleOpacity) {
options.circleOpacity = options.opacity
}
if (!options.circleWeight) {
options.circleWeight = options.weight
}
if (!options.fillColor) {
options.fillColor = options.color
}
if (!options.fontSize) {
options.fontSize = Number(options.iconSize.x/4)
}
if (!options.iconAnchor) {
options.iconAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.y))
}
else {
options.iconAnchor = L.point(options.iconAnchor)
}
if (!options.popupAnchor) {
options.popupAnchor = L.point(0, (-0.75)*(options.iconSize.y))
}
else {
options.popupAnchor = L.point(options.popupAnchor)
}
options.html = this._createSVG()
},
_createCircle: function() {
var cx = Number(this.options.circleAnchor.x)
var cy = Number(this.options.circleAnchor.y)
var radius = this.options.iconSize.x/2 * Number(this.options.circleRatio)
var fill = this.options.circleFillColor
var fillOpacity = this.options.circleFillOpacity
var stroke = this.options.circleColor
var strokeOpacity = this.options.circleOpacity
var strokeWidth = this.options.circleWeight
var className = this.options.className + "-circle"
var circle = '<circle class="' + className + '" cx="' + cx + '" cy="' + cy + '" r="' + radius +
'" fill="' + fill + '" fill-opacity="'+ fillOpacity +
'" stroke="' + stroke + '" stroke-opacity=' + strokeOpacity + '" stroke-width="' + strokeWidth + '"/>'
return circle
},
_createPathDescription: function() {
var height = Number(this.options.iconSize.y)
var width = Number(this.options.iconSize.x)
var weight = Number(this.options.weight)
var margin = weight / 2
var startPoint = "M " + margin + " " + (width/2) + " "
var leftLine = "L " + (width/2) + " " + (height - weight) + " "
var rightLine = "L " + (width - margin) + " " + (width/2) + " "
var arc = "A " + (width/4) + " " + (width/4) + " 0 0 0 " + margin + " " + (width/2) + " Z"
var d = startPoint + leftLine + rightLine + arc
return d
},
_createPath: function() {
var pathDescription = this._createPathDescription()
var strokeWidth = this.options.weight
var stroke = this.options.color
var strokeOpacity = this.options.opacity
var fill = this.options.fillColor
var fillOpacity = this.options.fillOpacity
var className = this.options.className + "-path"
var path = '<path class="' + className + '" d="' + pathDescription +
'" stroke-width="' + strokeWidth + '" stroke="' + stroke + '" stroke-opacity="' + strokeOpacity +
'" fill="' + fill + '" fill-opacity="' + fillOpacity + '"/>'
return path
},
_createSVG: function() {
var path = this._createPath()
var circle = this._createCircle()
var text = this._createText()
var className = this.options.className + "-svg"
var style = "width:" + this.options.iconSize.x + "px; height:" + this.options.iconSize.y + "px;"
var svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="' + className + '" style="' + style + '">' + path + circle + text + '</svg>'
return svg
},
_createText: function() {
var fontSize = this.options.fontSize + "px"
var fontWeight = this.options.fontWeight
var lineHeight = Number(this.options.fontSize)
var x = this.options.circleAnchor.x
var y = this.options.circleAnchor.y + (lineHeight * 0.35) //35% was found experimentally
var circleText = this.options.circleText
var textColor = this.options.fontColor.replace("rgb(", "rgba(").replace(")", "," + this.options.fontOpacity + ")")
var text = '<text text-anchor="middle" x="' + x + '" y="' + y + '" style="font-size: ' + fontSize + '; font-weight: ' + fontWeight +'" fill="' + textColor + '">' + circleText + '</text>'
return text
}
})
L.divIcon.svgIcon = function(options) {
return new L.DivIcon.SVGIcon(options)
}
L.Marker.SVGMarker = L.Marker.extend({
options: {
"iconFactory": L.divIcon.svgIcon,
"iconOptions": {}
},
initialize: function(latlng, options) {
options = L.Util.setOptions(this, options)
options.icon = options.iconFactory(options.iconOptions)
this._latlng = latlng
},
onAdd: function(map) {
L.Marker.prototype.onAdd.call(this, map)
},
setStyle: function(style) {
if (this._icon) {
var svg = this._icon.children[0]
var iconBody = this._icon.children[0].children[0]
var iconCircle = this._icon.children[0].children[1]
if (style.color && !style.iconOptions) {
var stroke = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.opacity+")")
var fill = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.fillOpacity+")")
iconBody.setAttribute("stroke", stroke)
iconBody.setAttribute("fill", fill)
iconCircle.setAttribute("stroke", stroke)
this.options.icon.fillColor = fill
this.options.icon.color = stroke
this.options.icon.circleColor = stroke
}
if (style.opacity) {
this.setOpacity(style.opacity)
}
if (style.iconOptions) {
if (style.color) { style.iconOptions.color = style.color }
var iconOptions = L.Util.setOptions(this.options.icon, style.iconOptions)
this.setIcon(L.divIcon.svgIcon(iconOptions))
}
}
}
})
L.marker.svgMarker = function(latlng, options) {
return new L.Marker.SVGMarker(latlng, options)
}

View File

@ -540,7 +540,7 @@ $( function() {
{% macro render_pagination(pagination) %}
{% if 'prev_url' in pagination or 'next_url' in pagination %}
<nav aria-label="Page navigation example">
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination['prev_url'] %}
<li class="page-item"> <a class="page-link" href="{{ pagination['prev_url'] }}">{{ _('Previous') }}</a></li>

View File

@ -5,31 +5,278 @@
{% endblock %}
{% block header %}
{{ render_google_filter_autocomplete_header() }}
<script>
var page = 1;
var per_page = 20;
var leaflet_map = null;
var map_cluster_group = null;
var events = null;
var arrayOfMarkers = [];
function init_leaflet_map() {
if (leaflet_map != null) {
return;
}
leaflet_map = L.map('map').fitWorld();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(leaflet_map);
L.control.locate({
keepCurrentZoomLevel: true,
icon: 'fa fa-location-arrow'
}).addTo(leaflet_map);
}
function clear_events_from_map() {
if (leaflet_map == null) {
return;
}
if (map_cluster_group != null) {
map_cluster_group.clearLayers();
}
arrayOfMarkers = [];
}
function show_event_in_list(event_id) {
var card = $('#event_card_' + event_id);
scroll_to_element(
card,
function () {
card.effect("highlight", {}, 500);
});
}
function fly_to_event(latitude, longitude) {
$('#map').show();
if (arrayOfMarkers.length == 0) {
add_events_to_map();
}
scroll_to_element(
$('#map'),
function () {
leaflet_map.flyTo([latitude, longitude], 16);
});
}
function add_events_to_map() {
init_leaflet_map();
clear_events_from_map();
$.each(events, function(index, event) {
if (event.location != null && event.location.geo != null) {
var start = moment(event.startDate);
var title = start.format("dd. DD.MM.YYYY LT") + ' ' + '<a href="#" onclick="show_event_in_list(\'' + event.identifier + '\'); return false;">' + event.name + '</a>';
var icon = new L.DivIcon.SVGIcon({
color: "#000",
fillColor: 'blue',
fillOpacity: 1.0,
iconSize: [28,41],
circleRatio: 0.35
});
var geo = event.location.geo;
var marker = L.marker([geo.latitude, geo.longitude], { icon: icon }).bindPopup(title);
arrayOfMarkers.push(marker);
}
});
if (arrayOfMarkers.length == 0) {
return;
}
var group_was_created = false;
if (map_cluster_group == null) {
map_cluster_group = L.markerClusterGroup({ disableClusteringAtZoom: 16});
group_was_created = true;
}
map_cluster_group.addLayers(arrayOfMarkers);
if (group_was_created) {
leaflet_map.addLayer(map_cluster_group);
}
leaflet_map.fitBounds(map_cluster_group.getBounds());
}
function toggle_map() {
if ($('#map').is(":visible")) {
$('#map').hide();
} else {
$('#map').show();
add_events_to_map();
}
}
function start_request() {
handle_request_start();
var result_list = $("#result_list");
result_list.empty();
var req_data = $("#filter_form").serialize();
req_data += '&page=' + page;
$.ajax({
url: "{{ url_for('api_event_dates') }}",
type: "get",
dataType: "json",
data: req_data,
error: function(xhr, status, error) {
handle_request_error(xhr, status, error);
},
success: function (data) {
var content = '';
events = data.event;
var event_count = events.length;
var cols = 3;
$.each(events, function(index, event) {
var is_card_deck = (index % cols == 0);
if (is_card_deck) {
if (content != '') {
content += '</div>';
}
content += '<div class="card-deck">';
}
var start = moment(event.startDate);
var img_tag = '';
if (event.image != null) {
img_tag = '<img src="' + event.image + '" class="card-img-top" style="object-fit: cover; height: 12vh; border-radius: 0;" />';
}
var map_link_start = '';
var map_link_end = '';
if (event.location != null && event.location.geo != null) {
var geo = event.location.geo;
map_link_start = '<a href="#" onclick="fly_to_event(' + geo.latitude + ', ' + geo.longitude + '); return false;">';
map_link_end = '</a>';
}
content += '<div class="card mb-3" id="event_card_' + event.identifier + '">' +
'<div class="card-header">' + start.format("dd. DD.MM.YYYY LT") + '</div>' +
img_tag +
'<div class="card-body" style="padding:1rem">' +
'<h5 class="card-title"><a href="' + event.url[0] + '">' + event.name + render_event_status_pill(event) + '</a></h5>' +
'<p class="card-text">' + event.description.truncate(100) + '</p>' +
'</div>' +
'<div class="card-footer text-muted small">' +
'<div class="text-muted mr-2"><i class="fa fa-database"></i> ' + event.organizer[0].name + '</div>' +
'<div class="text-muted">' + map_link_start + '<i class="fa fa-map-marker"></i> ' + event.location.name + map_link_end + '</div>' +
'</div>' +
'</div>';
});
if (content != '') {
var remaining_cols = cols - (event_count % cols);
for (i = 0; i < remaining_cols; i++) {
content += '<div class="card mb-3"></div>';
}
content += '</div>';
}
result_list.append(content);
if (page == 1) {
$('#prev_item').addClass('disabled');
} else {
$('#prev_item').removeClass('disabled');
}
if (event_count < per_page) {
$('#next_item').addClass('disabled');
} else {
$('#next_item').removeClass('disabled');
}
handle_request_success();
if (leaflet_map != null) {
add_events_to_map();
}
}
});
}
$( function() {
$("#filter_form").submit(function(e) {
e.stopPropagation();
page = 1;
start_request();
return false;
});
$("#prev_link").click(function(){
if (page > 1) {
page--;
start_request();
}
return false;
});
$("#next_link").click(function(){
page++;
start_request();
return false;
});
$('#map').hide();
$("#filter_form").submit();
});
</script>
{% endblock %}
{% block content %}
<h1>{{ _('Event Dates') }} <button type="button" class="btn btn-outline-secondary mx-2" data-toggle="collapse" data-target="#search_form" aria-expanded="{% if form.is_submitted() %}true{% else %}false{% endif %}"><i class="fa fa-filter"></i></button></h1>
<h1>
{{ _('Event Dates') }}
<button type="button" class="btn btn-outline-secondary mx-2" data-toggle="collapse" data-target="#search_form" aria-expanded="{% if form.is_submitted() %}true{% else %}false{% endif %}"><i class="fa fa-filter"></i></button>
<button type="button" class="btn btn-outline-secondary" onclick="toggle_map()"><i class="fa fa-map"></i></button>
</h1>
<div class="{% if form.is_submitted() %}{% else %}collapse{% endif %}" id="search_form">
{{ render_event_dates_filter_form(form) }}
</div>
{% for date in dates %}
<div class="card mb-3" style="max-width: 768px;">
{% if date.event.photo_id %}
<img src="{{ url_for('image', id=date.event.photo_id) }}" class="card-img-top" style="object-fit: cover; height: 20vh;" />
{% endif %}
<div class="card-body" style="padding:1rem">
<h5 class="card-title">{{ date.event.name }}{{ render_event_status_pill(date.event) }}</h5>
<h6 class="card-subtitle mb-2 text-muted"><i class="fa fa-calendar"></i> {{ date.start | datetimeformat('short') }}</h6>
<p class="card-text">{{ date.event.description | truncate(100) }}</p>
<small class="text-muted mr-2"><i class="fa fa-database"></i> {{ date.event.admin_unit.name }}</small>
<small class="text-muted"><i class="fa fa-map-marker"></i> {{ date.event.event_place.name }}</small>
<a href="{{ url_for('event_date', id=date.id) }}" class="stretched-link"></a>
<div class="row">
<div class="col-md mx-3 mb-3" id="map" style="height: 400px;">
</div>
<div class="col-md">
<div id="result_container">
<div class="m-1 small text-center text-secondary" id="result_info"></div>
<div style="min-height: 300px;">
<div id="result_list">
</div>
</div>
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item disabled" id="prev_item"><a class="page-link" id="prev_link" href="#">{{ _('Previous') }}</a></li>
<li class="page-item disabled" id="next_item"><a class="page-link" id="next_link" href="#">{{ _('Next') }}</a></li>
</ul>
</nav>
</div>
<div class="spinner-border m-3" role="status" id="spinner" style="display: none;">
<span class="sr-only">Loading...</span>
</div>
<div class="alert alert-danger m-3" role="alert" id="error_alert" style="display: none;"></div>
</div>
</div>
{% endfor %}
{{ render_pagination(pagination) }}
{% endblock %}

View File

@ -18,6 +18,10 @@
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='jquery.recurrenceinput.css')}}" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol@0.68.0/dist/L.Control.Locate.min.css" />
{%- block styles %}
{%- endblock styles %}
@ -27,8 +31,33 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/1.0.6/jsrender.js" integrity="sha512-DtWD/uCp2x1mspIw6vhZGPuJP85aKEL0HFI0jFVxcEiWw1OHJn36LqtkX9G9chzKVYLtTT0G8uJ2miDEL7ucTQ==" crossorigin="anonymous"></script>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol@0.68.0/dist/L.Control.Locate.min.js" charset="utf-8"></script>
<script src="{{ url_for('static', filename='jquery.recurrenceinput.js')}}"></script>
<script src="{{ url_for('static', filename='site.js')}}"></script>
<script src="{{ url_for('static', filename='svg-icon.js')}}"></script>
<script>
function render_event_status_pill(event) {
if (event.eventStatus != null) {
if (event.eventStatus == "EventCancelled") {
return '<span class="badge badge-pill badge-warning">{{ _('EventStatus.cancelled') }}</span>';
}
if (event.eventStatus == "EventMovedOnline") {
return '<span class="badge badge-pill badge-warning">{{ _('EventStatus.movedOnline') }}</span>';
}
if (event.eventStatus == "EventPostponed") {
return '<span class="badge badge-pill badge-warning">{{ _('EventStatus.postponed') }}</span>';
}
if (event.eventStatus == "EventRescheduled") {
return '<span class="badge badge-pill badge-warning">{{ _('EventStatus.rescheduled') }}</span>';
}
}
return '';
}
</script>
<title>{% block title %}{{ title|default }}{% endblock title %}</title>

View File

@ -30,13 +30,9 @@ def event_dates():
else:
flash_errors(form)
dates = get_event_dates_query(params).paginate()
return render_template('event_date/list.html',
form=form,
params=params,
dates=dates.items,
pagination=get_pagination_urls(dates))
params=params)
@app.route('/eventdate/<int:id>')
def event_date(id):