map_awigo.php 14 KB

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