map_template.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <?php
  2. // prepared data for testing (reduces workload on awigo API :D )
  3. $jsonData = file_get_contents('http://area51.cybob.com/praktikum/daniel/agent/data/map_template_data.json');
  4. $orte = json_decode($jsonData, true);
  5. $orteJsArray = json_encode($orte, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  6. // ### HTML-Response
  7. $html = '
  8. <!DOCTYPE html>
  9. <html lang="de">
  10. <head>
  11. <link rel="stylesheet" href="https://area51.cybob.com/praktikum/daniel/agent/leaflet/leaflet.css">
  12. <link rel="stylesheet" href="https://area51.cybob.com/praktikum/daniel/agent/css/map.css">
  13. <script src="https://area51.cybob.com/praktikum/daniel/agent/leaflet/leaflet.js"></script>
  14. <style>
  15. .leaflet-marker-icon {
  16. width: 25px !important;
  17. }
  18. </style>
  19. </head>
  20. <body>
  21. <div id="map-wrapper">
  22. <div id="zipcode-search-wrapper">
  23. <div class="input-with-clear">
  24. <input
  25. type="input"
  26. id="zipcode-input"
  27. placeholder="PLZ"
  28. inputmode="numeric"
  29. list="zipcode-datalist"
  30. maxlength="5"
  31. autocomplete="true"
  32. />
  33. <button id="zipcode-search-close" type="button" aria-label="Eingabe löschen">
  34. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  35. <line x1="1" y1="1" x2="15" y2="15" stroke="currentColor" stroke-width="2"/>
  36. <line x1="15" y1="1" x2="1" y2="15" stroke="currentColor" stroke-width="2"/>
  37. </svg>
  38. </button>
  39. </div>
  40. <button id="zipcode-search-button" type="button">🔍</button>
  41. </div>
  42. <datalist id="zipcode-datalist"></datalist>
  43. <div id="map-info">
  44. <button id="map-info-close" aria-label="Schließen" title="Schließen">
  45. <img src="https://area51.cybob.com/praktikum/daniel/agent/images/x.svg" alt="Schließen"/>
  46. </button>
  47. <a id="map-link" href="#" target="_blank" title="In Google Maps öffnen" style="display:inline-flex;align-items:center;gap:4px;">
  48. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="input-close">
  49. <path d="M4.146 8.354a.5.5 0 0 1 0-.708L9.793 2H6.5a.5.5 0 0 1 0-1h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V2.707L4.854 8.854a.5.5 0 0 1-.708 0z"/>
  50. <path d="M13.5 14a.5.5 0 0 1-.5.5H3a2 2 0 0 1-2-2V3.5a.5.5 0 0 1 1 0V12.5a1 1 0 0 0 1 1h10a.5.5 0 0 1 .5.5z"/>
  51. </svg>
  52. <span>Google Maps</span>
  53. </a>
  54. <h4 id="map-info-title">Bitte einen Ort auswählen!</h4>
  55. <span id="map-info-street"></span><br>
  56. <span id="map-info-plz"></span><br>
  57. <span><a id="map-info-telephone"></a></span><br>
  58. <br>
  59. <div id="map-info-opening-hours-weekdays"></div>
  60. <div id="map-info-description"></div>
  61. </div>
  62. <div id="map"></div>
  63. </div>
  64. <script>
  65. // ### Default is in Osnabrueck ###
  66. const defaultView = {
  67. lat: 52.2799,
  68. lng: 8.0472,
  69. zoom: 9,
  70. zoomClose: 16
  71. };
  72. // ### Default Offset for compact view ###
  73. const LAT_OFFSET = 0.0011;
  74. const MIN_DEVIATION = 0.00003;
  75. const COMPACT_THRESHOLD = 500; // px
  76. // ### State Memory ###
  77. const mapState = {
  78. isZoomed: false,
  79. isDragging: false,
  80. isCompact: false
  81. };
  82. // ### Leaflet Map Object ###
  83. let map = null;
  84. // ### Markers with queried json info (ort) ###
  85. let markers = [];
  86. function hideMarkers(markers) {
  87. markers.forEach(marker => {
  88. const markerElement = marker.getElement();
  89. const shadow = marker._shadow;
  90. if (markerElement) {
  91. markerElement.style.display = "none";
  92. }
  93. if (shadow) {
  94. shadow.style.display = "none"
  95. }
  96. })
  97. }
  98. function hideMarkerShadows(markers) {
  99. markers.forEach(marker => {
  100. const shadow = marker._shadow;
  101. if (shadow) shadow.style.display = "none";
  102. })
  103. }
  104. function showMarkers(markers) {
  105. markers.forEach(marker => {
  106. const markerElement = marker.getElement();
  107. if (markerElement) {
  108. markerElement.style.display = "";
  109. }
  110. })
  111. }
  112. // Close InfoBox with small animation
  113. function closeInfoBox() {
  114. const infoBox = document.getElementById("map-info");
  115. if (!infoBox.classList.contains("active")) {
  116. return;
  117. }
  118. infoBox.classList.remove("active");
  119. infoBox.classList.add("closing");
  120. setTimeout(() => {
  121. infoBox.classList.remove("closing");
  122. }, 125);
  123. }
  124. // check for user position => add marker and center map
  125. function checkUserPosition() {
  126. if (navigator.geolocation) {
  127. navigator.geolocation.getCurrentPosition(function(position) {
  128. setTimeout(() => {
  129. userLat = position.coords.latitude;
  130. userLng = position.coords.longitude;
  131. map.setView([userLat, userLng], 10);
  132. map.invalidateSize();
  133. }, 500)
  134. }, function(error) {
  135. console.warn("Geolocation not found");
  136. });
  137. } else {
  138. console.warn("Geolocation not supported by this browser.");
  139. }
  140. }
  141. function loadLeafletScript(callback) {
  142. var script = document.createElement("script");
  143. script.src = "https://area51.cybob.com/praktikum/daniel/agent/leaflet/leaflet.js";
  144. script.onload = callback;
  145. document.head.appendChild(script);
  146. console.log("Leaflet script loaded");
  147. }
  148. // Fly to LatLng Location with optional offset for compact View
  149. // Map Marker will then be centered, as the info box with appear from the bottom
  150. function flyToWithOptionalOffset(latLng, zoom) {
  151. let newLat = latLng.lat;
  152. if (mapState.isCompact) {
  153. newLat -= LAT_OFFSET; // adjust latitude for compact view
  154. }
  155. map.flyTo({lat: newLat, lng: latLng.lng}, zoom, {
  156. animate: true,
  157. duration: 0.5
  158. });
  159. }
  160. function getVisibleMapCenter() {
  161. const center = map.getCenter().clone();
  162. if (mapState.isCompact) {
  163. center.lat -= LAT_OFFSET;
  164. }
  165. return center;
  166. }
  167. function getLogicalMapCenter() {
  168. let center = map.getCenter().clone();
  169. if (mapState.isCompact) {
  170. center.lat += LAT_OFFSET;
  171. }
  172. return center;
  173. }
  174. function updateInfoBox(ort) {
  175. // replace info box strings
  176. document.getElementById("map-info-title").innerHTML = ort.title;
  177. document.getElementById("map-info-description").innerHTML = ort.description || "";
  178. document.getElementById("map-info-opening-hours-weekdays").innerHTML = ort.openingTimes;
  179. document.getElementById("map-info-street").innerHTML = ort.address || "";
  180. document.getElementById("map-info-plz").innerHTML = (ort.zip + " " + ort.city) || "";
  181. const tel = ort.telephone.replaceAll(" ", "")
  182. .replaceAll(")", "")
  183. .replaceAll("(", "")
  184. .replaceAll("-", "");
  185. document.getElementById("map-info-telephone").innerHTML = ("Tel.: " + ort.telephone);
  186. document.getElementById("map-info-telephone").href = "tel:" + tel;
  187. // build & assign google maps url
  188. const lat = ort.lat;
  189. const lng = ort.lng;
  190. document.getElementById("map-link").href = `https://www.google.com/maps?q=${lat},${lng}`;
  191. }
  192. function showInfoBox() {
  193. document.getElementById("map-info").classList.add("active");
  194. }
  195. function isMapDeviationReached(marker) {
  196. const logicalCenter = getLogicalMapCenter();
  197. const deltaLat = Math.abs(logicalCenter.lat - marker.getLatLng().lat);
  198. const deltaLng = Math.abs(logicalCenter.lng - marker.getLatLng().lng);
  199. return deltaLat >= MIN_DEVIATION || deltaLng >= MIN_DEVIATION;
  200. }
  201. function initializeMap() {
  202. // initialize the map
  203. map = L.map("map", {
  204. zoomAnimation: true,
  205. markerZoomAnimation: true,
  206. fadeAnimation: true,
  207. attributionControl: true,
  208. }).setView([52.2799, 8.0472], defaultView.zoom);
  209. L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
  210. attribution: "&copy; <a href=\\"https://www.openstreetmap.org/copyright\\">OpenStreetMap</a>"
  211. }).addTo(map);
  212. // Checks with each resize if the map-wrapper is smaller than threshold
  213. function updateMapLayoutResponsiveness() {
  214. const mapWrapper = document.getElementById("map-wrapper");
  215. const mapDiv = document.getElementById("map");
  216. const isCompact = mapWrapper.offsetWidth < COMPACT_THRESHOLD;
  217. if (isCompact) {
  218. mapWrapper.classList.add("compact");
  219. mapDiv.classList.add("compact");
  220. map.invalidateSize();
  221. mapState.isCompact = true;
  222. } else {
  223. mapWrapper.classList.remove("compact");
  224. mapDiv.classList.remove("compact");
  225. mapState.isCompact = false;
  226. }
  227. }
  228. updateMapLayoutResponsiveness();
  229. window.addEventListener("resize", updateMapLayoutResponsiveness);
  230. document.getElementById("map").addEventListener("mousedown", () => {
  231. mapState.isDragging = false;
  232. })
  233. document.getElementById("map").addEventListener("mousemove", () => {
  234. mapState.isDragging = true;
  235. })
  236. document.getElementById("map").addEventListener("click", (e) => {
  237. if (
  238. e.target.classList.contains("map-info") ||
  239. e.target.classList.contains("leaflet-marker-icon") ||
  240. e.target.classList.contains("leaflet-marker-icon") ||
  241. mapState.isDragging
  242. ) {
  243. return;
  244. }
  245. closeInfoBox();
  246. })
  247. document.getElementById("map-info-close").addEventListener("click", () => {
  248. closeInfoBox();
  249. });
  250. // standard-icon (blue)
  251. L.Icon.Default.mergeOptions({
  252. iconRetinaUrl: "marker-icon-default-green.png",
  253. iconUrl: "marker-icon-default-green.png",
  254. shadowUrl: "marker-icon-shadow-default.png",
  255. className: "awigo-marker-icon"
  256. });
  257. // add marker from json-array
  258. orte.forEach(function(ort) {
  259. // remove shadows for AWIGO
  260. var icon = new L.Icon.Default();
  261. const marker = L.marker([ort.lat, ort.lng], {icon: icon}).addTo(map);
  262. marker.ort = ort;
  263. markers.push(marker);
  264. // Event-Listener for click on marker
  265. marker.on("click", function() {
  266. updateInfoBox(ort);
  267. if (isMapDeviationReached(marker)) {
  268. showInfoBox();
  269. flyToWithOptionalOffset(marker.getLatLng(), defaultView.zoomClose);
  270. mapState.isZoomed = true;
  271. } else if (mapState.isZoomed){
  272. flyToWithOptionalOffset({lat: defaultView.lat, lng: defaultView.lng}, defaultView.zoom)
  273. closeInfoBox();
  274. mapState.isZoomed = false;
  275. }
  276. });
  277. });
  278. // re-render map (prevents missing tile issues)
  279. map.invalidateSize();
  280. // checkUserPosition();
  281. hideMarkerShadows(markers);
  282. }
  283. // ### Program start ###
  284. // filter for green places from AWIGO API
  285. let orte =' . $orteJsArray . ';
  286. let zipcodes = [...new Set(orte.map(o => o.zip))].sort();
  287. // Input Select
  288. const input = document.getElementById("zipcode-input");
  289. const datalist = document.getElementById("zipcode-datalist");
  290. // add available zipcodes to datalist
  291. zipcodes.forEach(zip => {
  292. const option = document.createElement("option");
  293. option.value = zip;
  294. datalist.appendChild(option);
  295. });
  296. function searchByZip(zip) {
  297. if (!zip || zip.length !== 5 || !/^\d{5}$/.test(zip)) {
  298. showMarkers(markers);
  299. return;
  300. }
  301. const filteredMarkers = markers.filter(marker => marker.ort.zip === zip);
  302. if (filteredMarkers.length > 0) {
  303. const firstMarker = filteredMarkers[0];
  304. if (isMapDeviationReached(firstMarker)) {
  305. flyToWithOptionalOffset(firstMarker.getLatLng(), defaultView.zoomClose);
  306. mapState.isZoomed = true;
  307. updateInfoBox(firstMarker.ort);
  308. showInfoBox();
  309. }
  310. filteredMarkers.forEach((marker) => {
  311. hideMarkers(markers);
  312. showMarkers(filteredMarkers);
  313. })
  314. }
  315. }
  316. input.addEventListener("input", () => {
  317. input.value = input.value.replace(/\D/g, "").slice(0,5);
  318. if (input.value.length === 5) {
  319. searchByZip(input.value);
  320. } else {
  321. showMarkers(markers);
  322. }
  323. });
  324. document.getElementById("zipcode-search-button").addEventListener("click", () => {
  325. const zip = input.value.trim();
  326. searchByZip(zip);
  327. });
  328. document.getElementById("zipcode-search-close").addEventListener("click", () => {
  329. input.value = "";
  330. showMarkers(markers);
  331. })
  332. // ### callback
  333. loadLeafletScript(initializeMap);
  334. </script>
  335. ';
  336. // JSON-Response aufbauen
  337. $dictResponse = [
  338. "success" => true,
  339. "status_code" => 200,
  340. "status_description" => "ok",
  341. "response" => $html
  342. ];
  343. $output = json_encode($dictResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  344. header('Content-Type: application/json');
  345. http_response_code($dictResponse["status_code"]);
  346. echo $output;
  347. exit;