Логин:   Пароль:






Новости
Рассылки
Форум
Поиск


Java
- Апплеты
- Вопрос-ответ
- Классы
- Примеры
- Руководства
- Статьи
- IDE
- Словарь терминов
- Скачать

Мобильная Java
- Игры
- Примеры
- Статьи
- WAP, WML и пр.

JavaScript
- Вопрос-ответ
- Примеры
- Статьи

Веб-мастеринг
- HTML
- CSS
- SSI

Разминка для ума
Проекты
Книги
Ссылки
Программы
Юмор :)




Rambler's Top100
Rambler's Top100

JavaScript: СтатьиПриглашаем на борт: AJAX, HTML-холст, и Суперпоезд

Приглашаем на борт: AJAX, HTML-холст, и Суперпоезд

HTML-элемент canvas («холст»), впервые появившийся в браузере Safari фирмы Apple, позволяет веб-разработчикам создавать двумерные рисунки при помощи JavaScript. Теперь, когда его поддержка появилась и в недавно вышедшем Firefox 1.5, HTML-холст стал использоваться ещё шире (этот элемент даже планируется включить в стандарт HTML 5). Дело осталось только за Microsoft: неизвестно, сколько времени пройдёт, прежде чем поддержка элемента canvas появится и в Internet Explorer. Однако я верю, что при продолжающемся сейчас движении в сторону Web 2.0 HTML-холст оказывается неожиданно полезным ресурсом для всех тех приложений, которые могут пожертвовать поддержкой IE, — особенно в свете появления сейчас JavaScript-библиотек, позволяющих на полную мощь использовать класс XMLHttpRequest. Я попробовал использовать canvas и XHR совместно, и это произвело на меня сильное впечатление (интересно отметить забавное совпадение: их разработчики — фирмы Apple и Microsoft соответственно) :-)

Моим первым экспериментом по сочетанию canvas и AJAX была попытка воспроизвести наглядный словарь синонимов от Thinkmap, используя только HTML, JavaScript, и (на серверной стороне) базу данных WordNet. У HTML-холста обнаружились некоторые ограничения, с которыми я был вынужден столкнуться — в первую очередь, его неспособность отображать текст. С другой стороны, обнаружились и его сильные стороны — такие, как простота и удобство интеграции с AJAX. То, что у меня получилось, вы можете увидеть по адресу awordlike.com.

Суперпоезд

В этой статье я собираюсь продемонстрировать вам менее сложный эксперимент, в котором HTML-холст будет использоваться для отображения в реальном времени состояния воображаемой железной дороги (загрузить файлы с примерами). Я не буду углубляться в детали использования JavaScript и Ruby; если вам требуется помощь в освоении этих языков, то материалы, с которыми вам стоит ознакомиться, приведены в разделе «Ссылки». Теперь перейдём к делу.

Штат Вашингтон завершил, наконец, создание Суперпоезда — системы пригородных поездов, призванной избавить окрестности Сиэтла от их знаменитых транспортных пробок. Частью этого проекта являлась разработка программного комплекса, передающего в центр управления Суперпоездом координаты каждого поезда. Команда разработчиков этого комплекса потрудилась на славу: создана система рассылки сообщений, позволяющая любому пользователю в сети Суперпоезда получать оповещения о состоянии поездов. К несчастью, они уделили меньше внимания пользовательскому интерфейсу этого комплекса, и диспетчеры Суперпоезда вынуждены довольствоваться текстовой веб-страничкой, которую всякий раз, когда требуется получить свежую информацию, нужно обновлять в браузере вручную. Моё задание состояло в создании нового пользовательского интерфейса, который бы графически отображал состояние всех поездов Суперпоезда в реальном времени. Сначала, в качестве эксперимента, требовалось создать новый интерфейс только для одной линии Суперпоезда. Но прежде чем я смог бы задействовать с клиентской стороны AJAX и canvas, нужен был способ опрашивать состояние поездов через веб-сервер. Я начал с малого: первым делом запустил Ruby WEBrick, и подключил замыкание docroot:

server.rb

require 'webrick'
include WEBrick

server = HTTPServer.new( :Port => 8053 )
server.mount("/", HTTPServlet::FileHandler, "./docroot")

server.mount_proc("/train/line") do |request, response|
  response['Content-Type'] = "text/plain"
  response.body = "toot, toot"
end

trap("INT") { server.shutdown }

server.start

Если вы запустите этот скрипт ( ruby server.rb) и откроете в браузере адрес http://localhost:8053/train/line, то вы должны увидеть это:

Рисунок 1
Рисунок 1.

Я создал папку docroot, в которой буду размещать HTML-файлы. Пока что пусть там лежит заглушка:

docroot/redwood.html

<html>
<body>
hello woodinville!
</body>
</html>

Теперь я создам замыкание /train/line, которое будет выводить чуть более полезную информацию. В качестве протокола между клиентом и сервером я использую JSON, потому что это самое простое, что только можно использовать из JavaScript.

server.rb

...
require 'trainspotter'
...
train_spotter = TrainSpotter.new

server.mount_proc("/train/line") do |request, response|
  response['Content-Type'] = "text/plain"

  json = train_spotter.status_report.
           map { |train| '{"track": "' + train.track.to_s + '", 
			"location": ' + train.location.to_s + '}' }.
             join ','

  response.body = "[ #{json} ]"
end
...

trainspotter.rb

class TrainSpotter
  def status_report
    [ Status.new("south", 20) ]
  end
end

class Status
  attr_reader :track, :location

  def initialize(track, location)
    @track = track
    @location = location
  end
end

Открыв в браузере страницу по адресу http://localhost:8053/train/line, теперь можно увидеть нечто лишь чуть более полезное но всё ещё впереди.

Рисунок 2
Рисунок 2.

То, что мне на самом деле нужно — чтобы объект TrainSpotter вёл себя так, как будто он содержит постоянно обновляющийся отчёт о состоянии поезда. Пока что я задам ему простую систему поведения, которая давала бы мне достаточно правдоподобные данные:

trainspotter.rb

TRACKS = [:north, :south]
TRAINS_PROGRESS = {:north => 5, :south => 420}
MAX_SPEED = 5

class TrainSpotter
  def status_report
    report = []

    TRAINS_PROGRESS[:north] += rand(MAX_SPEED)
    report << Status.new("north", TRAINS_PROGRESS[:north])

    TRAINS_PROGRESS[:south] -= rand(MAX_SPEED)
    report << Status.new("south", TRAINS_PROGRESS[:south])
  end
end
...

Теперь, когда я открываю в браузере адрес http://localhost:8053/train/line и обновляю страницу несколько раз подряд, я вижу, как данные изменяются! Прекрасно видно, как один поезд едет на юг, из Вудинвилля в Редмонд, а другой — на север, из Редмонда в Вудинвилль.

Рисунок 3
Рисунок 3.

Следующий этап — задействование AJAX в redwood.html, чтобы не приходилось постоянно обновлять страницу вручную. Идеально подходит для этого невероятно простая библиотека Prototype:

docroot/redwood.html

<html>
<head>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
</head>
<body>

<div id="status"></div>
<script type="text/javascript">
  new Ajax.PeriodicalUpdater($("status"), "/train/line")
</script>

</body>
</html>

Открыв в браузере страницу http://localhost:8053/redwood.html, я теперь вижу, что состояние поездов обновляется каждые две секунды (таков интервал опроса по умолчанию у объекта Ajax.PeriodicalUpdater библиотеки Prototype). Здорово! Но думаю, что всего этого ещё недостаточно, чтобы поразить заказчика. Настало время заменить отчёт о состоянии динамически обновляющимся рисунком на клиентской стороне. Я по-прежнему начинаю с малого — то есть, как и следует в любом железнодорожном проекте, с рельсов:

docroot/redwood.html


...
<body>

<canvas
  id="redwood"
  width="500"
  height="120"
  style="border: 1px solid black">
</canvas>

<script type="text/javascript">
  var tracks = {
    north: new Track(30),
    south: new Track(85)
  }

  var canvas = undefined

  // IE will return false here
  if ($("redwood").getContext) {
    canvas = $("redwood").getContext("2d")
    drawTracks()
  }

  function drawTracks() {
    $H(tracks).values().each(function(track) {
        track.draw()
    })
  }

  function Track(y) {
    this.y = y
    this.startX = 10
    this.endX = 490
    this.tieSize = 3
    this.tieGap = 5
    this.draw = drawTrack
  }

  function drawTrack() {
    canvas.moveTo(this.startX, this.y)
    canvas.beginPath()
    var x = this.startX
    while (x < this.endX) {
      canvas.lineTo(x, this.y)
      canvas.lineTo(x, this.y + this.tieSize)
      canvas.moveTo(x, this.y)
      canvas.lineTo(x, this.y - this.tieSize)
      canvas.moveTo(x, this.y)
      x = x + this.tieGap
    }
    canvas.closePath()
    canvas.stroke()
  }
</script>

<div id="status"></div>
...

Рисунок 4
Рисунок 4.

Обратите внимание, что я не пользуюсь для организации циклов обычным синтаксисом JavaScript. Раз уж я использую для AJAX JavaScript-библиотеку Prototype (1.4.0), то логично воспользоваться встроенными в неё итераторами коллекций в стиле Ruby и другими синтаксическими приятностями, такими как $(), $H().values() и each(). Сейчас, когда рельсы уложены, нужно пустить по ним сами поезда. Первым делом я откажусь от использования объекта Ajax.PeriodicalUpdater и заменю его вызовом window.setInterval ( setIntervalиграет важную роль в организации динамического обновления HTML-холста). Ещё я оформлю drawTracks в виде функции более высокого уровня — updateCanvas.

docroot/redwood.html

  ...
  if ($("redwood").getContext) {
    canvas = $("redwood").getContext("2d")
    window.setInterval(updateCanvas, 1000 * 2)
    updateCanvas()
  }

  function updateCanvas() {
    clearScreen()
    drawTracks()
  }

  function clearScreen() {
    canvas.clearRect(0, 0, $("redwood").width, $("redwood").height)
  }

  function drawTracks() {
  ...

Холст уже автоматически обновляется каждые 2000 миллисекунд (т.е. 2 секунды), но поездов ещё нет. Займёмся теперь ими. Картинки, изображающие поезда, можно наносить на HTML-холст методом drawImage элемента canvas. Сейчас, когда регулярное обновление производится методом window.setInterval, я могу считывать состояние поездов при помощи самого низкоуровневого объекта библиотеки Prototype  — Ajax.Request. Как только я получу от сервера данные, я обновляю расположения картинок, изображающих поезда. Приведённый здесь код динамически обновляет положение поездов, отображая их движение в реальном времени:

docroot/redwood.html

  var trains = {
    north: new Train("train-lr.png", 5),
    south: new Train("train-rl.png", 60)
  }
  ...
  function updateCanvas() {
    clearScreen()
    drawTracks()

    new Ajax.Request("/train/line",
                     { onComplete: function(request) {
                         var jsonData = eval(request.responseText)
                         if (jsonData == undefined) { return }
                         jsonData.each(function(data) {
                           trains[data.track].update(data.location)
                         })
                       }
                     })
  }
  ...
  function Train(image, y) {
    this.image = new Image()
    this.image.src = image
    this.y = y
    this.update = updateTrain
  }

  function updateTrain(location) {
    canvas.drawImage(this.image, location, this.y)
  }
  ...

Рисунок 5
Рисунок 5.

На этом этапе мы соединили вместе несколько механизмов. Используя Prototype, мы выполняем асинхронный вызов, получаем в ответ JSON-строку, и вызовом функции eval() преобразуем эту строку в массив JavaScript-объектов. Объекты Train описывают состояние каждого поезда и действия, необходимые для его отображения. У приведённого кода есть недостаток — неприятное мерцание изображений поездов. Оно происходит из-за того, что между очисткой экрана и вызовом события onComplete , обработчик которого рисует поезда, проходит некоторое время. Так что избавиться от надоедливого мерцания просто — достаточно вызывать clearScreen в самый последний момент перед отрисовкой поездов.

docroot/redwood.html

  ...
  function updateCanvas() {
    new Ajax.Request("/train/line",
                     { onComplete: function(request) {
                         var jsonData = eval(request.responseText)
                         if (jsonData == undefined) { return }
                         clearScreen()
                         jsonData.each(function(data) {
                           trains[data.track].update(data.location)
                         })
                         drawTracks()
                         drawHotspots()
                       }
                     })
  }
  ...

Осталось добавить последнюю деталь. Расположения поездов — это лишь один из типов информации о линии Суперпоезда, которую мы можем отображать в нашей системе. Кроме этого, программный комплекс Суперпоезда отслеживает «горячие точки» — те участки железнодорожного пути, на которых происходили столкновения или незапланированные остановки. Я начну добавление в нашу систему этих горячих точек с того, что жёстко задам их расположения в классе TrainSpotter и подключу к WEBrick новое замыкание, которое бы передавало эти расположения веб-клиенту.

server.rb

...
server.mount_proc("/train/line") do |request, response|
  response['Content-Type'] = "text/plain"
  json = train_spotter.status_report.
           map { |train| '{"track": "' + train.track.to_s + '", 
			"location": ' + train.location.to_s + '}' }.
             join ','
  response.body = "[ #{json} ]"
end

server.mount_proc("/train/hotspots") do |request, response|
  response['Content-Type'] = "text/plain"
  json = train_spotter.hot_spots
           map { |train| '{"track": "' + train.track.to_s + '", 
			"location": ' + train.location.to_s + '}' }.
             join ','
  response.body = "[ #{json} ]"
end
...

trainspotter.rb

class TrainSpotter
...
  def hot_spots
    [ Status.new(:north, 125), Status.new(:south, 250), Status.new(:south, 150) ]
  end
end

Надеюсь, вы не удовлетворены приведённым кодом замыкания /train/hotspots — действительно, он практически совпадает с кодом /train/line. Можно избавиться от возникшего дублирования, выделив в отдельную функцию преобразование объектов Status в JSON-строки.

server.rb

...
def status_list_to_json(list)
  json = list.
           map { |train| '{"track": "' + train.track.to_s + '", 
			"location": ' + train.location.to_s + '}' }.
             join ','
  "[ #{json} ]"
end

server.mount_proc("/train/line") do |request, response|
  response['Content-Type'] = "text/plain"
  response.body = status_list_to_json(train_spotter.status_report)
end

server.mount_proc("/train/hotspots") do |request, response|
  response['Content-Type'] = "text/plain"
  response.body = status_list_to_json(train_spotter.hot_spots)
end
...

Если вы сейчас откроете в браузере адрес http://localhost:8053/train/hotspots, то вы увидите это:

Рисунок 6
Рисунок 6.

Теперь надо добавить отображение горячих точек на клиентской стороне. Поскольку их расположения меняются не так часто, как расположения поездов, то я буду использовать отдельный вызов window.setInterval, чтобы запрашивать у сервера данные о горячих точках раз в час.

docroot/redwood.html


...
<script type="text/javascript">
  ...
  var hotspots = []
  var canvas = undefined

  if ($("redwood").getContext) {
    canvas = $("redwood").getContext("2d")
    window.setInterval(updateCanvas, 1000 * 2)
    window.setInterval(updateHotspots, 1000 * 60 * 60)
    updateCanvas()
    updateHotspots()
  }

  function updateHotspots() {
    new Ajax.Request("/train/hotspots",
                     { onComplete: function(request) {
                         hotspots = eval(request.responseText)
                       }
                     })
  }

  function updateCanvas() {
    new Ajax.Request("/train/line",
                     { onComplete: function(request) {
                         var jsonData = eval(request.responseText)
                         if (jsonData == undefined) { return }
                         clearScreen()
                         jsonData.each(function(data) {
                           trains[data.track].update(data.location)
                         })
                         drawTracks()
                         drawHotspots()
                       }
                     })
  }
  ...
  function drawHotspots() {
    hotspots.each(function(hotspot) {
      tracks[hotspot.track].drawHotspot(hotspot.location)
    })
  }

  function Track(y) {
    ...
    this.hotspotRadius = 6
    this.hotspotColor = "red"
    this.drawHotspot = drawHotspot
  }
  ...
  function drawHotspot(location) {
    canvas.moveTo(location, this.y)
    canvas.beginPath()
    canvas.fillStyle = this.hotspotColor
    canvas.arc(location, this.y, this.hotspotRadius, 0, Math.PI,
false)
    canvas.closePath()
    canvas.fill()
  }
  ...
</script>
...

Рисунок 7
Рисунок 7.

Эксперимент успешно завершён! Заказчик, представляющий штат Вашингтон, полностью удовлетворён созданной системой. Теперь моё задание состоит в интеграции класса TrainSpotter с используемой в сети Суперпоезда системой рассылки сообщений. Если моя система покажет себя на настоящей линии Вудинвилль—Редмонд достаточно хорошо, то мне предстоит ещё немало работы над этим комплексом…

Ссылки


Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 11

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/books/show2b.php on line 30
Узнай о чем ты на самом деле сейчас думаешь тут.


[an error occurred while processing this directive]



Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 91

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/news/worldnews.php on line 95