Гео-метки

This commit is contained in:
n1rwana 2023-08-03 23:30:01 +03:00
parent a2384cc231
commit da863c25f3
14 changed files with 297 additions and 4 deletions

View file

@ -246,5 +246,22 @@ class Post extends Postable
$this->save(); $this->save();
} }
function getGeo(): ?object
{
if (!$this->getRecord()->geo) return NULL;
return (object) json_decode($this->getRecord()->geo, true, JSON_UNESCAPED_UNICODE);
}
function getLat(): ?float
{
return (float) $this->getRecord()->geo_lat ?? NULL;
}
function getLon(): ?float
{
return (float) $this->getRecord()->geo_lon ?? NULL;
}
use Traits\TRichText; use Traits\TRichText;
} }

View file

@ -0,0 +1,10 @@
SELECT *,
SQRT(
POW(69.1 * (? - geo_lat), 2) +
POW(69.1 * (? - geo_lon) * COS(RADIANS(geo_lat)), 2)
) AS distance
FROM Posts
WHERE id <> ?
HAVING distance < 1 AND distance IS NOT NULL
ORDER BY distance
LIMIT 25;

View file

@ -293,7 +293,16 @@ final class WallPresenter extends OpenVKPresenter
} }
} }
if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note) $geo = NULL;
if (!is_null($this->postParam("geo")) && $this->postParam("geo") != "none") {
$geo = json_decode($this->postParam("geo"), true, JSON_UNESCAPED_UNICODE);
if (!$geo["lat"] || !$geo["lng"] || !$geo["name"]) {
$this->flashFail("err", tr("error"), "Ошибка при прикреплении геометки");
}
}
if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note && !$geo)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
@ -305,6 +314,11 @@ final class WallPresenter extends OpenVKPresenter
$post->setAnonymous($anon); $post->setAnonymous($anon);
$post->setFlags($flags); $post->setFlags($flags);
$post->setNsfw($this->postParam("nsfw") === "on"); $post->setNsfw($this->postParam("nsfw") === "on");
if ($geo) {
$post->setGeo(json_encode($geo));
$post->setGeo_Lat($geo["lat"]);
$post->setGeo_Lon($geo["lng"]);
}
$post->save(); $post->save();
} catch (\LengthException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
@ -480,4 +494,49 @@ final class WallPresenter extends OpenVKPresenter
# TODO localize message based on language and ?act=(un)pin # TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment"));
} }
function renderNearest(int $wall, int $post_id): void
{
if ($_SERVER["REQUEST_METHOD"] !== "POST") $this->notFound();
$this->assertUserLoggedIn();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post)
$this->notFound();
$lat = $post->getLat();
$lon = $post->getLon();
if (!$lat || !$lon)
$this->returnJson(["success" => false, "error" => "У поста не указана гео-метка"]);
$query = file_get_contents(__DIR__ . "/../Models/sql/get-nearest-posts.tsql");
$_posts = DatabaseConnection::i()->getContext()->query($query, $lat, $lon, $post->getId())->fetchAll();
$posts = [];
foreach ($_posts as $post) {
$distance = $post["distance"];
$post = (new Posts)->get($post["id"]);
if (!$post || $post->isDeleted()) continue;
$owner = $post->getOwner();
$preview = mb_substr($post->getText(), 0, 50) . (strlen($post->getText()) > 50 ? "..." : "");
$posts[] = [
"preview" => strlen($preview) > 0 ? $preview : "(нет текста)",
"url" => "/wall" . $post->getPrettyId(),
"time" => $post->getPublicationTime()->html(),
"owner" => [
"url" => $owner->getURL(),
"avatar_url" => $owner->getAvatarURL(),
"name" => $owner->getCanonicalName(),
"verified" => $owner->isVerified(),
"writes" => ($owner instanceof User) ? ($owner->isFemale() ? tr("post_writes_f") : tr("post_writes_m")) : tr("post_writes_m")
],
"geo" => $post->getGeo(),
"distance" => $distance
];
}
$this->returnJson(["success" => true, "posts" => $posts, "need_count" => count($posts) === 25]);
}
} }

View file

@ -16,7 +16,7 @@
</div> </div>
<div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog"> <div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog">
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true, notes => true} {include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true, notes => true, geo => true}
</div> </div>
{foreach $posts as $post} {foreach $posts as $post}

View file

@ -16,7 +16,7 @@
<div class="content_divider"> <div class="content_divider">
<div> <div>
<div n:if="$canPost" class="content_subtitle"> <div n:if="$canPost" class="content_subtitle">
{include "../components/textArea.xml", route => "/wall$owner/makePost"} {include "../components/textArea.xml", route => "/wall$owner/makePost", geo => true}
</div> </div>
<div class="content"> <div class="content">

View file

@ -73,6 +73,10 @@
</div> </div>
</div> </div>
</div> </div>
<div n:if="$post->getGeo()" style="padding: 4px;">
<div style="border-bottom: #ECECEC solid 1px;" />
<div style="cursor: pointer; padding: 4px;" onclick="javascript:openGeo({$post->getGeo()}, {$post->getOwner()->getId()}, {$post->getVirtualId()})"><b>Геометка</b>: {$post->getGeo()->name}</div>
</div>
<div n:if="$post->isAd()" style="color:grey;"> <div n:if="$post->isAd()" style="color:grey;">
<br/> <br/>
&nbsp;! Этот пост был размещён за взятку. &nbsp;! Этот пост был размещён за взятку.

View file

@ -67,6 +67,10 @@
</div> </div>
</div> </div>
</div> </div>
<div n:if="$post->getGeo()" style="padding: 4px;">
<div style="border-bottom: #ECECEC solid 1px;" />
<div style="cursor: pointer; padding: 4px;" onclick="javascript:openGeo({$post->getGeo()}, {$post->getOwner()->getId()}, {$post->getVirtualId()})"><b>Геометка</b>: {$post->getGeo()->name}</div>
</div>
<div n:if="$post->isAd()" style="color:grey;"> <div n:if="$post->isAd()" style="color:grey;">
<br/> <br/>
&nbsp;! Этот пост был размещён за взятку. &nbsp;! Этот пост был размещён за взятку.

View file

@ -16,6 +16,9 @@
</div> </div>
<div class="post-has-note"> <div class="post-has-note">
</div>
<div class="post-has-geo">
</div> </div>
<div n:if="$postOpts ?? true" class="post-opts"> <div n:if="$postOpts ?? true" class="post-opts">
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']} {var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
@ -58,6 +61,7 @@
<input n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" /> <input n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
<input type="hidden" name="poll" value="none" /> <input type="hidden" name="poll" value="none" />
<input type="hidden" id="note" name="note" value="none" /> <input type="hidden" id="note" name="note" value="none" />
<input type="hidden" id="geo" name="geo" value="none" />
<input type="hidden" name="type" value="1" /> <input type="hidden" name="type" value="1" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<br/> <br/>
@ -91,6 +95,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" />
{_poll} {_poll}
</a> </a>
<a n:if="$geo ?? false" href="javascript:initGeo({$textAreaId})">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/apps/amarok.png" />
Гео-метка
</a>
</div> </div>
</div> </div>
</div> </div>
@ -113,3 +121,10 @@
{script "js/vnd_literallycanvas.js"} {script "js/vnd_literallycanvas.js"}
{css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"} {css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"}
{/if} {/if}
{if $geo}
{script "js/node_modules/leaflet/dist/leaflet.js"}
{css "js/node_modules/leaflet/dist/leaflet.css"}
{script "js/node_modules/leaflet-control-geocoder/dist/Control.Geocoder.js"}
{css "js/node_modules/leaflet-control-geocoder/dist/Control.Geocoder.css"}
{/if}

View file

@ -8,7 +8,7 @@
</div> </div>
<div> <div>
<div n:if="$canPost" class="content_subtitle"> <div n:if="$canPost" class="content_subtitle">
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true} {include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true, geo => true}
</div> </div>
<div class="content"> <div class="content">

View file

@ -137,6 +137,8 @@ routes:
handler: "Wall->delete" handler: "Wall->delete"
- url: "/wall{num}_{num}/pin" - url: "/wall{num}_{num}/pin"
handler: "Wall->pin" handler: "Wall->pin"
- url: "/wall{num}_{num}/nearest"
handler: "Wall->nearest"
- url: "/blob_{text}/{?path}.{text}" - url: "/blob_{text}/{?path}.{text}"
handler: "Blob->file" handler: "Blob->file"
placeholders: placeholders:

View file

@ -263,3 +263,162 @@ async function showArticle(note_id) {
u("body").removeClass("dimmed"); u("body").removeClass("dimmed");
u("body").addClass("article"); u("body").addClass("article");
} }
async function initGeo(tid) {
MessageBox("Прикрепить геометку", "<div id=\"osm-map\"></div>", ["Прикрепить", "Отмена"], [(function () {
let marker = {
lat: currentMarker._latlng.lat,
lng: currentMarker._latlng.lng,
name: currentMarker._popup._content
};
$(`#post-buttons${tid} #geo`).val(JSON.stringify(marker));
$(`#post-buttons${tid} .post-has-geo`).text(`Геометка: ${marker.name}`);
$(`#post-buttons${tid} .post-has-geo`).show();
}), Function.noop]);
const element = document.getElementById('osm-map');
element.style = 'height: 600px;';
let markerLayers = L.layerGroup();
let map = L.map(element, {
center: [55.322978, 38.673362],
zoom: 10,
attributionControl: false,
width: 800
});
let currentMarker = null;
markerLayers.addTo(map);
map.on('click', (e) => {
let lat = e.latlng.lat;
let lng = e.latlng.lng;
if (currentMarker) map.removeLayer(currentMarker);
$.get({
url: `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=jsonv2`,
success: (response) => {
markerLayers.clearLayers();
currentMarker = L.marker([lat, lng]).addTo(map);
currentMarker.bindPopup(response?.name ?? response?.display_name).openPopup();
markerLayers.addLayer(currentMarker);
}
})
});
const geocoderControl = L.Control.geocoder({
defaultMarkGeocode: false,
}).addTo(map);
geocoderControl.on('markgeocode', function (e) {
console.log(e);
let lat = e.geocode.properties.lat;
let lng = e.geocode.properties.lon;
let name = (e.geocode.properties?.name ?? e.geocode.properties.display_name);
if (currentMarker) map.removeLayer(currentMarker);
currentMarker = L.marker([lat, lng]).addTo(map);
currentMarker.bindPopup(name).openPopup();
console.log("Широта: " + lat + ", Долгота: " + lng);
console.log("Название места: " + name);
let marker = {
lat: lat,
lng: lng,
name: name
};
map.setView([lat, lng], 15);
});
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
$(".ovk-diag-cont").width('50%');
setTimeout(function(){ map.invalidateSize()}, 100);
}
function openGeo(data, owner_id, virtual_id) {
MessageBox("Геометка", "<div id=\"osm-map\"></div>", ["Ближайшие посты", "OK"], [(function () {
getNearPosts(owner_id, virtual_id);
}), Function.noop]);
let element = document.getElementById('osm-map');
element.style = 'height: 600px;';
let map = L.map(element, {attributionControl: false});
let target = L.latLng(data.lat, data.lng);
map.setView(target, 15);
let marker = L.marker(target).addTo(map);
marker.bindPopup(data.name).openPopup();
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
$(".ovk-diag-cont").width('50%');
setTimeout(function(){ map.invalidateSize()}, 100);
}
function getNearPosts(owner_id, virtual_id) {
$.ajax({
type: "POST",
url: `/wall${owner_id}_${virtual_id}/nearest`,
success: (response) => {
if (response.success) {
openNearPosts(response);
} else {
MessageBox("Ошибка", "Произошла ошибка в ходе запроса:" + (response?.error ?? "Неизвестная ошибка"), ["OK"], [Function.noop]);
}
}
});
}
function openNearPosts(posts) {
console.log(posts);
let MsgBody = "";
posts.posts.forEach((post) => {
MsgBody += `<a style="color: inherit; display: block; margin-bottom: 8px;" href="${post.url}" target="_blank">
<table border="0" style="font-size: 11px;" class="post">
<tbody>
<tr>
<td width="54" valign="top">
<a href="${post.owner.url}">
<img src="${post.owner.avatar_url}" width="50">
</a>
</td>
<td width="100%" valign="top">
<div class="post-author">
<a href="${post.owner.url}"><b>${post.owner.name}</b></a>
${post.owner.verified ? `<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">` : ""}
${post.owner.writes}
<br>
<a href="${post.url}" class="date">
${post.time}
</a>
</div>
<div class="post-content" id="2_28">
<div class="text" id="text2_28">
${post.preview}
</div>
<div style="padding: 4px;">
<div style="border-bottom: #ECECEC solid 1px;"></div>
<div style="cursor: pointer; padding: 4px;"><b>Геометка</b>: ${post.geo.name}</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</a>`;
});
if (posts.need_count) MsgBody += "<br/><br/><center style='color: grey;'>Показаны первые 25 постов</center>"
MessageBox("Ближайшие посты", MsgBody, ["OK"], [Function.noop]);
}

View file

@ -6,6 +6,8 @@
"jquery": "^3.0.0", "jquery": "^3.0.0",
"knockout": "^3.5.1", "knockout": "^3.5.1",
"ky": "^0.19.0", "ky": "^0.19.0",
"leaflet": "^1.9.4",
"leaflet-control-geocoder": "^2.4.0",
"literallycanvas": "^0.5.2", "literallycanvas": "^0.5.2",
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"msgpack-lite": "^0.1.26", "msgpack-lite": "^0.1.26",

View file

@ -173,6 +173,18 @@ ky@^0.19.0:
resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a" resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a"
integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw== integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw==
leaflet-control-geocoder@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/leaflet-control-geocoder/-/leaflet-control-geocoder-2.4.0.tgz#f6c00ae00b53d2ac5908e874a9aefd414f615f22"
integrity sha512-b2QlxuFd40uIDbnoUI3U9fzfnB4yKUYlmsXjquJ2d2YjoJqnyVYcIJeErAVv3kPvX3nI0gzvBq1XHMgSVFrGkQ==
optionalDependencies:
open-location-code "^1.0.0"
leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
literallycanvas@^0.5.2: literallycanvas@^0.5.2:
version "0.5.2" version "0.5.2"
resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd" resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd"
@ -225,6 +237,11 @@ object-assign@^4.1.0, object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
open-location-code@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/open-location-code/-/open-location-code-1.0.3.tgz#5ea1a34ee5221c6cafa04392e1bd906fd7488f7e"
integrity sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==
plotly.js-dist@^1.52.3: plotly.js-dist@^1.52.3:
version "1.52.3" version "1.52.3"
resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-1.52.3.tgz#4c16c6da6adab6cdba169087b5005bdddbf10834" resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-1.52.3.tgz#4c16c6da6adab6cdba169087b5005bdddbf10834"

View file

@ -0,0 +1,4 @@
ALTER TABLE `Posts`
ADD `geo` LONGTEXT NULL DEFAULT NULL AFTER `deleted`,
ADD `geo_lat` DECIMAL(10, 8) NULL DEFAULT NULL AFTER `geo`,
ADD `geo_lon` DECIMAL(10, 8) NULL DEFAULT NULL AFTER `geo_lat`;